clerk/internal/lsp/textdocument_selection_range.go (view raw)
Oleksandr Smirnov
Oleksandr Smirnov
olexsmir@gmail.com remove some more pointers i missed before, 1 month ago
olexsmir@gmail.com remove some more pointers i missed before, 1 month ago
| 1 | package lsp |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | |
| 6 | "go.lsp.dev/protocol" |
| 7 | |
| 8 | "olexsmir.xyz/clerk/internal/lsp/lsputil" |
| 9 | "olexsmir.xyz/clerk/journal" |
| 10 | "olexsmir.xyz/clerk/journal/ast" |
| 11 | "olexsmir.xyz/clerk/journal/token" |
| 12 | ) |
| 13 | |
| 14 | func (s *server) SelectionRange(_ context.Context, params *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) { |
| 15 | u := params.TextDocument.URI |
| 16 | state, ok := s.getDocState(u) |
| 17 | if !ok { |
| 18 | return nil, nil |
| 19 | } |
| 20 | an := s.analysisFor(u) |
| 21 | if an == nil { |
| 22 | return nil, nil |
| 23 | } |
| 24 | pf := parsedFileFor(an, u.Path()) |
| 25 | if pf == nil { |
| 26 | return nil, nil |
| 27 | } |
| 28 | |
| 29 | li := state.lineIdx |
| 30 | docSel := protocol.SelectionRange{Range: li.SpanRange(fullDocSpan(pf.Src))} |
| 31 | out := make([]protocol.SelectionRange, len(params.Positions)) |
| 32 | for i, pos := range params.Positions { |
| 33 | cursor := li.Offset(int(pos.Line), int(pos.Character)) |
| 34 | out[i] = selectionAt(state.text, pf, li, cursor, docSel) |
| 35 | } |
| 36 | return out, nil |
| 37 | } |
| 38 | |
| 39 | func fullDocSpan(src []byte) token.Span { return token.Span{End: token.Pos{Offset: len(src)}} } |
| 40 | |
| 41 | func selectionAt(content string, pf *journal.ParsedFile, li *lsputil.LineIndex, cursor int, docSel protocol.SelectionRange) protocol.SelectionRange { |
| 42 | e := entryAt(pf.Ast.Entries, cursor) |
| 43 | if e == nil { |
| 44 | return docSel |
| 45 | } |
| 46 | es := entrySpan(e) |
| 47 | if !spanContains(content, es, cursor) { |
| 48 | return docSel |
| 49 | } |
| 50 | switch c := e.(type) { |
| 51 | case *ast.BlankLine: |
| 52 | return docSel |
| 53 | case *ast.Comment: // the comment span is the entry span itself: no extra level |
| 54 | return commentOrParent(content, c, li, cursor, docSel) |
| 55 | } |
| 56 | parent := protocol.SelectionRange{Range: li.SpanRange(es), Parent: &docSel} |
| 57 | return selectionInEntry(content, e, li, cursor, parent) |
| 58 | } |
| 59 | |
| 60 | func selectionInEntry(content string, e ast.Entry, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange { |
| 61 | switch t := e.(type) { |
| 62 | case *ast.Transaction: |
| 63 | return transactionSelection(content, t, li, cursor, parent) |
| 64 | case *ast.PeriodicTransaction: |
| 65 | return periodicSelection(content, t, li, cursor, parent) |
| 66 | case *ast.AutomatedTransaction: |
| 67 | return automatedSelection(content, t, li, cursor, parent) |
| 68 | case *ast.AccountDirective: |
| 69 | return accountDirectiveSelection(content, t, li, cursor, parent) |
| 70 | case *ast.CommodityDirective: |
| 71 | return commodityDirectiveSelection(content, t, li, cursor, parent) |
| 72 | case *ast.PayeeDirective: |
| 73 | if t.Name != nil { |
| 74 | if sel, ok := selectionForSpan(content, li, t.Name.Span, cursor, parent); ok { |
| 75 | return sel |
| 76 | } |
| 77 | } |
| 78 | return commentOrParent(content, t.Comment, li, cursor, parent) |
| 79 | case *ast.TagDirective: |
| 80 | if sp, ok := tagDirectiveSpan(content, t); ok { |
| 81 | if sel, ok := selectionForSpan(content, li, sp, cursor, parent); ok { |
| 82 | return sel |
| 83 | } |
| 84 | } |
| 85 | return commentOrParent(content, t.Comment, li, cursor, parent) |
| 86 | case *ast.AliasDirective: |
| 87 | if sel, ok := accountSelection(content, &t.From, li, cursor, parent); ok { |
| 88 | return sel |
| 89 | } |
| 90 | if sel, ok := accountSelection(content, &t.To, li, cursor, parent); ok { |
| 91 | return sel |
| 92 | } |
| 93 | return commentOrParent(content, t.Comment, li, cursor, parent) |
| 94 | case *ast.DefaultCommodityDirective: |
| 95 | if sel, ok := amountSelection(content, &t.Amount, li, cursor, parent); ok { |
| 96 | return sel |
| 97 | } |
| 98 | return commentOrParent(content, t.Comment, li, cursor, parent) |
| 99 | case *ast.MarketPriceDirective: |
| 100 | if sel, ok := selectionForSpan(content, li, t.DateTime.Date.Span, cursor, parent); ok { |
| 101 | return sel |
| 102 | } |
| 103 | if t.DateTime.Time != nil { |
| 104 | if sel, ok := selectionForSpan(content, li, t.DateTime.Time.Span, cursor, parent); ok { |
| 105 | return sel |
| 106 | } |
| 107 | } |
| 108 | if sel, ok := amountSelection(content, &t.Amount, li, cursor, parent); ok { |
| 109 | return sel |
| 110 | } |
| 111 | return commentOrParent(content, t.Comment, li, cursor, parent) |
| 112 | case *ast.ConversionDirective: |
| 113 | if sel, ok := amountSelection(content, &t.From, li, cursor, parent); ok { |
| 114 | return sel |
| 115 | } |
| 116 | if sel, ok := amountSelection(content, &t.To, li, cursor, parent); ok { |
| 117 | return sel |
| 118 | } |
| 119 | return commentOrParent(content, t.Comment, li, cursor, parent) |
| 120 | } |
| 121 | return parent |
| 122 | } |
| 123 | |
| 124 | func transactionSelection(content string, t *ast.Transaction, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange { |
| 125 | var header [6]token.Span // date, second date, status, code, payee, note |
| 126 | sps := append(header[:0], t.Date.Span) |
| 127 | if t.SecondDate != nil { |
| 128 | sps = append(sps, t.SecondDate.Span) |
| 129 | } |
| 130 | if t.Status.Value != ast.StatusNone { |
| 131 | sps = append(sps, t.Status.Span) |
| 132 | } |
| 133 | if t.Code != nil { |
| 134 | sps = append(sps, t.Code.Span) |
| 135 | } |
| 136 | if t.Payee != nil { |
| 137 | sps = append(sps, t.Payee.Span) |
| 138 | } |
| 139 | if t.Note != nil { |
| 140 | sps = append(sps, t.Note.Span) |
| 141 | } |
| 142 | for _, sp := range sps { |
| 143 | if sel, ok := selectionForSpan(content, li, sp, cursor, parent); ok { |
| 144 | return sel |
| 145 | } |
| 146 | } |
| 147 | return commentsAndPostingsSelection(content, t.Comment, t.HeaderComments, t.Postings, li, cursor, parent) |
| 148 | } |
| 149 | |
| 150 | // commentsAndPostingsSelection descends into an entry's inline comment, header |
| 151 | // comments, then postings; returns parent when none contains the cursor. |
| 152 | func commentsAndPostingsSelection(content string, inline *ast.Comment, headers []*ast.Comment, postings []ast.Posting, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange { |
| 153 | if sel, ok := commentSelection(content, inline, li, cursor, parent); ok { |
| 154 | return sel |
| 155 | } |
| 156 | for _, c := range headers { |
| 157 | if sel, ok := commentSelection(content, c, li, cursor, parent); ok { |
| 158 | return sel |
| 159 | } |
| 160 | } |
| 161 | if sel, ok := postingsSelection(content, postings, li, cursor, parent); ok { |
| 162 | return sel |
| 163 | } |
| 164 | return parent |
| 165 | } |
| 166 | |
| 167 | func periodicSelection(content string, pt *ast.PeriodicTransaction, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange { |
| 168 | if sel, ok := selectionForSpan(content, li, pt.Period.Span, cursor, parent); ok { |
| 169 | if d := pt.Period.From; d != nil { |
| 170 | if sub, ok := selectionForSpan(content, li, d.Span, cursor, sel); ok { |
| 171 | return sub |
| 172 | } |
| 173 | } |
| 174 | if d := pt.Period.To; d != nil { |
| 175 | if sub, ok := selectionForSpan(content, li, d.Span, cursor, sel); ok { |
| 176 | return sub |
| 177 | } |
| 178 | } |
| 179 | return sel |
| 180 | } |
| 181 | if pt.Description != nil { |
| 182 | if sel, ok := selectionForSpan(content, li, pt.Description.Span, cursor, parent); ok { |
| 183 | return sel |
| 184 | } |
| 185 | } |
| 186 | return commentsAndPostingsSelection(content, pt.Comment, pt.HeaderComments, pt.Postings, li, cursor, parent) |
| 187 | } |
| 188 | |
| 189 | func automatedSelection(content string, at *ast.AutomatedTransaction, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange { |
| 190 | if sel, ok := selectionForSpan(content, li, at.Expr.Span, cursor, parent); ok { |
| 191 | return sel |
| 192 | } |
| 193 | return commentsAndPostingsSelection(content, at.Comment, at.HeaderComments, at.Postings, li, cursor, parent) |
| 194 | } |
| 195 | |
| 196 | func accountDirectiveSelection(content string, d *ast.AccountDirective, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange { |
| 197 | if sel, ok := accountSelection(content, &d.Account, li, cursor, parent); ok { |
| 198 | return sel |
| 199 | } |
| 200 | for i := range d.Subdirectives { |
| 201 | sd := &d.Subdirectives[i] |
| 202 | if sd.Kind == ast.SubdirectiveComment { |
| 203 | if sel, ok := commentSelection(content, sd.Comment, li, cursor, parent); ok { |
| 204 | return sel |
| 205 | } |
| 206 | continue |
| 207 | } |
| 208 | if sel, ok := selectionForSpan(content, li, sd.ValueSpan, cursor, parent); ok { |
| 209 | return sel |
| 210 | } |
| 211 | } |
| 212 | return commentOrParent(content, d.Comment, li, cursor, parent) |
| 213 | } |
| 214 | |
| 215 | func commodityDirectiveSelection(content string, d *ast.CommodityDirective, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange { |
| 216 | if sel, ok := selectionForSpan(content, li, d.CommoditySpan, cursor, parent); ok { |
| 217 | return sel |
| 218 | } |
| 219 | if d.FormatSub != nil { |
| 220 | if sel, ok := amountSelection(content, &d.FormatSub.Amount, li, cursor, parent); ok { |
| 221 | return sel |
| 222 | } |
| 223 | } |
| 224 | return commentOrParent(content, d.Comment, li, cursor, parent) |
| 225 | } |
| 226 | |
| 227 | // postingsSelection returns the selection inside the posting containing cursor. |
| 228 | func postingsSelection(content string, postings []ast.Posting, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) (protocol.SelectionRange, bool) { |
| 229 | for i := range postings { |
| 230 | p := &postings[i] |
| 231 | postingSel, ok := selectionForSpan(content, li, p.Span, cursor, parent) |
| 232 | if !ok { |
| 233 | continue |
| 234 | } |
| 235 | return postingSelection(content, postings[i], li, cursor, postingSel), true |
| 236 | } |
| 237 | return protocol.SelectionRange{}, false |
| 238 | } |
| 239 | |
| 240 | func postingSelection(content string, p ast.Posting, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange { |
| 241 | if p.Status.Value != ast.StatusNone { |
| 242 | if sel, ok := selectionForSpan(content, li, p.Status.Span, cursor, parent); ok { |
| 243 | return sel |
| 244 | } |
| 245 | } |
| 246 | if sel, ok := accountSelection(content, &p.Account, li, cursor, parent); ok { |
| 247 | return sel |
| 248 | } |
| 249 | if sel, ok := amountSelection(content, p.Amount, li, cursor, parent); ok { |
| 250 | return sel |
| 251 | } |
| 252 | if p.Cost != nil { |
| 253 | if sel, ok := selectionForSpan(content, li, p.Cost.Span, cursor, parent); ok { |
| 254 | return sel |
| 255 | } |
| 256 | } |
| 257 | if p.Balance != nil { |
| 258 | if sel, ok := selectionForSpan(content, li, p.Balance.Span, cursor, parent); ok { |
| 259 | return sel |
| 260 | } |
| 261 | } |
| 262 | if sel, ok := commentSelection(content, p.Comment, li, cursor, parent); ok { |
| 263 | return sel |
| 264 | } |
| 265 | for i := range p.Comments { |
| 266 | if sel, ok := commentSelection(content, &p.Comments[i], li, cursor, parent); ok { |
| 267 | return sel |
| 268 | } |
| 269 | } |
| 270 | return parent |
| 271 | } |
| 272 | |
| 273 | func accountSelection(content string, a *ast.Account, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) (protocol.SelectionRange, bool) { |
| 274 | accountSel, ok := selectionForSpan(content, li, a.Span, cursor, parent) |
| 275 | if !ok { |
| 276 | return protocol.SelectionRange{}, false |
| 277 | } |
| 278 | if len(a.Name) <= 1 { |
| 279 | return accountSel, true |
| 280 | } |
| 281 | for i := range a.Name { |
| 282 | if sel, ok := selectionForSpan(content, li, a.Name[i].Span, cursor, accountSel); ok { |
| 283 | return sel, true |
| 284 | } |
| 285 | } |
| 286 | return accountSel, true |
| 287 | } |
| 288 | |
| 289 | func amountSelection(content string, am *ast.Amount, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) (protocol.SelectionRange, bool) { |
| 290 | if am == nil { |
| 291 | return protocol.SelectionRange{}, false |
| 292 | } |
| 293 | amountSel, ok := selectionForSpan(content, li, am.Span, cursor, parent) |
| 294 | if !ok { |
| 295 | return protocol.SelectionRange{}, false |
| 296 | } |
| 297 | if sel, ok := selectionForSpan(content, li, am.CommoditySpan, cursor, amountSel); ok { |
| 298 | return sel, true |
| 299 | } |
| 300 | qStart, qEnd := quantitySpan(content, am) |
| 301 | if qEnd > qStart { |
| 302 | if sel, ok := selectionForSpan(content, li, token.Span{Start: token.Pos{Offset: qStart}, End: token.Pos{Offset: qEnd}}, cursor, amountSel); ok { |
| 303 | return sel, true |
| 304 | } |
| 305 | } |
| 306 | return amountSel, true |
| 307 | } |
| 308 | |
| 309 | // selectionForSpan selects span nested in parent when span contains cursor. |
| 310 | func selectionForSpan(content string, li *lsputil.LineIndex, span token.Span, cursor int, parent protocol.SelectionRange) (protocol.SelectionRange, bool) { |
| 311 | if !spanContains(content, span, cursor) { |
| 312 | return protocol.SelectionRange{}, false |
| 313 | } |
| 314 | return protocol.SelectionRange{Range: li.SpanRange(span), Parent: &parent}, true |
| 315 | } |
| 316 | |
| 317 | // commentSelection returns a selection inside comment when the cursor is on it: |
| 318 | // the tag key when the cursor is on a tag, else the whole comment line. |
| 319 | func commentSelection(content string, c *ast.Comment, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) (protocol.SelectionRange, bool) { |
| 320 | if c == nil { |
| 321 | return protocol.SelectionRange{}, false |
| 322 | } |
| 323 | commentSel, ok := selectionForSpan(content, li, c.Span, cursor, parent) |
| 324 | if !ok { |
| 325 | return protocol.SelectionRange{}, false |
| 326 | } |
| 327 | if ref := tagRefInComment(content, c, cursor); ref != nil { |
| 328 | return protocol.SelectionRange{Range: li.SpanRange(ref.span), Parent: &commentSel}, true |
| 329 | } |
| 330 | return commentSel, true |
| 331 | } |
| 332 | |
| 333 | // commentOrParent is commentSelection with parent as the fallback. |
| 334 | func commentOrParent(content string, c *ast.Comment, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange { |
| 335 | if sel, ok := commentSelection(content, c, li, cursor, parent); ok { |
| 336 | return sel |
| 337 | } |
| 338 | return parent |
| 339 | } |