snapshot of 962fc3c82c774ab371e3fc4e84c55ef26e61268d Annotations about the code that implements koment.

internal/lsp/workspace.go

1 package lsp
2
3 import (
4 "errors"
5 "fmt"
6 "io/fs"
7 "net/url"
8 "path/filepath"
9 "runtime"
10 "sort"
11 "strings"
12 "unicode/utf16"
13 "unicode/utf8"
14
15 "github.com/koment-dev/koment/internal/anchor"
16 "github.com/koment-dev/koment/internal/application"
17 "github.com/koment-dev/koment/internal/commentpolicy"
18 "github.com/koment-dev/koment/internal/policy"
19 "github.com/koment-dev/koment/internal/repository"
20 "github.com/koment-dev/koment/internal/store"
21 )
22
23 type workspaceFile struct {
24 root string
25 relative string
26 content []byte
27 service *application.Service
28 store *store.Store
29 }
30
31 func loadWorkspaceFile(uri string, content []byte) (workspaceFile, error) {
32 absolute, err := pathFromURI(uri)
33 if err != nil {
34 return workspaceFile{}, err
35 }
36 root, err := store.FindRoot(filepath.Dir(absolute))
37 if err != nil {
38 return workspaceFile{}, err
39 }
40 annotations := store.Open(root)
41 relative, err := annotations.FromWorkingDirectory(absolute)
42 if err != nil {
43 return workspaceFile{}, err
44 }
45 if content == nil {
46 content, err = annotations.ReadSource(relative)
47 if err != nil {
48 return workspaceFile{}, fmt.Errorf("reading %s: %w", relative, err)
49 }
50 }
51 entry := repository.Repository{ID: filepath.Base(root), Name: filepath.Base(root), Root: root}
52 return workspaceFile{
53 root: root, relative: relative, content: content,
54 service: application.NewService(entry), store: annotations,
55 }, nil
56 }
57
58 func pathFromURI(uri string) (string, error) {
59 parsed, err := url.Parse(uri)
60 if err != nil || parsed.Scheme != "file" {
61 return "", fmt.Errorf("URI %q is not a local file", uri)
62 }
63 value, err := url.PathUnescape(parsed.EscapedPath())
64 if err != nil {
65 return "", fmt.Errorf("decoding URI %q: %w", uri, err)
66 }
67 if runtime.GOOS == "windows" && len(value) >= 3 && value[0] == '/' && value[2] == ':' {
68 value = value[1:]
69 }
70 return filepath.Clean(filepath.FromSlash(value)), nil
71 }
72
73 func annotationViews(file workspaceFile) ([]application.AnnotationView, error) {
74 records, err := file.store.ForFile(file.relative)
75 if err != nil {
76 return nil, err
77 }
78 snapshot, err := application.AssembleSnapshot(application.SnapshotInput{
79 Repository: application.RepositoryIdentity{ID: filepath.Base(file.root), Name: filepath.Base(file.root)},
80 Records: records, Sources: map[string][]byte{file.relative: file.content},
81 })
82 if err != nil {
83 return nil, err
84 }
85 resolved, exists := snapshot.File(file.relative)
86 if !exists {
87 return nil, nil
88 }
89 return resolved.Annotations, nil
90 }
91
92 func annotationItems(file workspaceFile) ([]annotationItem, error) {
93 views, err := annotationViews(file)
94 if err != nil {
95 return nil, err
96 }
97 items := make([]annotationItem, 0, len(views))
98 for _, view := range views {
99 line := max(1, view.Line)
100 if view.Record.Spec.Anchor.Scope == store.ScopeFile {
101 line = 1
102 }
103 annotationRange := rangeValue{
104 Start: position{Line: line - 1},
105 End: position{Line: line - 1, Character: lineUTF16Length(file.content, line-1)},
106 }
107 items = append(items, annotationItem{
108 ID: view.Record.Metadata.ID, Kind: string(view.Record.Spec.Type),
109 Title: view.Record.Headline(), Body: view.Record.Spec.Body,
110 Status: string(view.Status), Line: line, Warning: view.Warning, Range: annotationRange,
111 })
112 }
113 sort.Slice(items, func(left, right int) bool {
114 if items[left].Line != items[right].Line {
115 return items[left].Line < items[right].Line
116 }
117 return items[left].ID < items[right].ID
118 })
119 return items, nil
120 }
121
122 func documentDiagnostics(file workspaceFile) ([]diagnostic, error) {
123 items, err := annotationItems(file)
124 if err != nil {
125 return nil, err
126 }
127 diagnostics := []diagnostic{}
128 for _, item := range items {
129 switch anchor.Status(item.Status) {
130 case anchor.StatusAmbiguous, anchor.StatusDrifted, anchor.StatusOrphaned:
131 diagnostics = append(diagnostics, diagnostic{
132 Range: item.Range, Severity: 1, Code: "koment." + item.Status,
133 Source: "koment", Message: item.Warning, Data: map[string]string{"id": item.ID},
134 })
135 }
136 }
137 if filepath.Ext(file.relative) != ".go" {
138 return diagnostics, nil
139 }
140 configured, err := policy.Load(file.root)
141 if errors.Is(err, fs.ErrNotExist) {
142 return diagnostics, nil
143 }
144 if err != nil {
145 return nil, err
146 }
147 records, err := file.store.ForFile(file.relative)
148 if err != nil {
149 return nil, err
150 }
151 violations, err := commentpolicy.CheckContent(file.relative, file.content, configured, records)
152 if err != nil {
153 return nil, err
154 }
155 for _, violation := range violations {
156 diagnostics = append(diagnostics, diagnostic{
157 Range: rangeFromOffsets(file.content, violation.Comment.Start, violation.Comment.End),
158 Severity: 2, Code: "koment.comment", Source: "koment",
159 Message: violation.Reason,
160 Data: map[string]any{
161 "comment": violation.Comment.Raw, "file": file.relative,
162 "autoPrompt": commentpolicy.IsCommentIntent(violation.Comment),
163 },
164 })
165 }
166 return diagnostics, nil
167 }
168
169 func rangeFromOffsets(content []byte, start, end int) rangeValue {
170 return rangeValue{Start: positionAt(content, start), End: positionAt(content, end)}
171 }
172
173 func positionAt(content []byte, offset int) position {
174 offset = min(max(offset, 0), len(content))
175 lineStart := 0
176 line := 0
177 for index, character := range content[:offset] {
178 if character == '\n' {
179 line++
180 lineStart = index + 1
181 }
182 }
183 units := 0
184 for remaining := content[lineStart:offset]; len(remaining) > 0; {
185 character, size := utf8.DecodeRune(remaining)
186 units += len(utf16.Encode([]rune{character}))
187 remaining = remaining[size:]
188 }
189 return position{Line: line, Character: units}
190 }
191
192 func lineUTF16Length(content []byte, wanted int) int {
193 start := 0
194 line := 0
195 for index, character := range content {
196 if line == wanted && character == '\n' {
197 return positionAt(content, index).Character
198 }
199 if character == '\n' {
200 line++
201 start = index + 1
202 }
203 }
204 if line == wanted {
205 return positionAt(content, len(content)).Character
206 }
207 _ = start
208 return 0
209 }
210
211 func markdown(item annotationItem) string {
212 var text strings.Builder
213 fmt.Fprintf(&text, "### %s\n\n**%s** · `%s` · `%s`\n\n%s", item.Title, item.Kind, item.Status, item.ID, item.Body)
214 if item.Warning != "" {
215 fmt.Fprintf(&text, "\n\n> %s", item.Warning)
216 }
217 return text.String()
218 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close