snapshot of eee7b5cfc0776c68816d7fdaa8af9d7a0e0da2e6 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/janpuc/koment/internal/anchor"
16 "github.com/janpuc/koment/internal/application"
17 "github.com/janpuc/koment/internal/commentpolicy"
18 "github.com/janpuc/koment/internal/policy"
19 "github.com/janpuc/koment/internal/repository"
20 "github.com/janpuc/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.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.ID, Kind: string(view.Record.Kind), Body: view.Record.Body,
109 Status: string(view.Status), Line: line, Warning: view.Warning, Range: annotationRange,
110 })
111 }
112 sort.Slice(items, func(left, right int) bool {
113 if items[left].Line != items[right].Line {
114 return items[left].Line < items[right].Line
115 }
116 return items[left].ID < items[right].ID
117 })
118 return items, nil
119 }
120
121 func documentDiagnostics(file workspaceFile) ([]diagnostic, error) {
122 items, err := annotationItems(file)
123 if err != nil {
124 return nil, err
125 }
126 diagnostics := []diagnostic{}
127 for _, item := range items {
128 switch anchor.Status(item.Status) {
129 case anchor.StatusAmbiguous, anchor.StatusDrifted, anchor.StatusOrphaned:
130 diagnostics = append(diagnostics, diagnostic{
131 Range: item.Range, Severity: 1, Code: "koment." + item.Status,
132 Source: "koment", Message: item.Warning, Data: map[string]string{"id": item.ID},
133 })
134 }
135 }
136 if filepath.Ext(file.relative) != ".go" {
137 return diagnostics, nil
138 }
139 configured, err := policy.Load(file.root)
140 if errors.Is(err, fs.ErrNotExist) {
141 return diagnostics, nil
142 }
143 if err != nil {
144 return nil, err
145 }
146 records, err := file.store.ForFile(file.relative)
147 if err != nil {
148 return nil, err
149 }
150 violations, err := commentpolicy.CheckContent(file.relative, file.content, configured, records)
151 if err != nil {
152 return nil, err
153 }
154 for _, violation := range violations {
155 diagnostics = append(diagnostics, diagnostic{
156 Range: rangeFromOffsets(file.content, violation.Comment.Start, violation.Comment.End),
157 Severity: 2, Code: "koment.comment", Source: "koment",
158 Message: violation.Reason,
159 Data: map[string]any{
160 "comment": violation.Comment.Raw, "file": file.relative,
161 "autoPrompt": commentpolicy.IsCommentIntent(violation.Comment),
162 },
163 })
164 }
165 return diagnostics, nil
166 }
167
168 func rangeFromOffsets(content []byte, start, end int) rangeValue {
169 return rangeValue{Start: positionAt(content, start), End: positionAt(content, end)}
170 }
171
172 func positionAt(content []byte, offset int) position {
173 offset = min(max(offset, 0), len(content))
174 lineStart := 0
175 line := 0
176 for index, character := range content[:offset] {
177 if character == '\n' {
178 line++
179 lineStart = index + 1
180 }
181 }
182 units := 0
183 for remaining := content[lineStart:offset]; len(remaining) > 0; {
184 character, size := utf8.DecodeRune(remaining)
185 units += len(utf16.Encode([]rune{character}))
186 remaining = remaining[size:]
187 }
188 return position{Line: line, Character: units}
189 }
190
191 func lineUTF16Length(content []byte, wanted int) int {
192 start := 0
193 line := 0
194 for index, character := range content {
195 if line == wanted && character == '\n' {
196 return positionAt(content, index).Character
197 }
198 if character == '\n' {
199 line++
200 start = index + 1
201 }
202 }
203 if line == wanted {
204 return positionAt(content, len(content)).Character
205 }
206 _ = start
207 return 0
208 }
209
210 func markdown(item annotationItem) string {
211 var text strings.Builder
212 fmt.Fprintf(&text, "**%s** · `%s` · `%s`\n\n%s", item.Kind, item.Status, item.ID, item.Body)
213 if item.Warning != "" {
214 fmt.Fprintf(&text, "\n\n> %s", item.Warning)
215 }
216 return text.String()
217 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close