-
-
Notifications
You must be signed in to change notification settings - Fork 24
/
doc.rkt
401 lines (353 loc) · 14.3 KB
/
doc.rkt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
#lang racket/base
(require "check-syntax.rkt"
"msg-io.rkt"
"responses.rkt"
"interfaces.rkt"
"scheduler.rkt"
"editor.rkt"
"path-util.rkt"
"doc-trace.rkt"
"struct.rkt"
"highlight.rkt"
racket/match
racket/class
racket/set
racket/list
racket/string
racket/bool
data/interval-map
syntax-color/module-lexer
syntax-color/racket-lexer
json
drracket/check-syntax
syntax/modread)
(struct Doc
(text
trace
uri
during-batch-change?
checked?)
#:transparent #:mutable)
(define (send-diagnostics doc diag-lst)
(display-message/flush (diagnostics-message (Doc-uri doc) diag-lst)))
;; the only place where really run check-syntax
(define (doc-run-check-syntax doc)
(define (task)
(set-Doc-checked?! doc #f)
(match-define (list new-trace diags)
(check-syntax (uri->path (Doc-uri doc)) (Doc-text doc) (Doc-trace doc)))
(send-diagnostics doc diags)
(set-Doc-trace! doc new-trace)
(set-Doc-checked?! doc #t))
(scheduler-push-task! (Doc-uri doc) task))
(define (lazy-check-syntax doc)
(when (not (Doc-during-batch-change? doc))
(doc-run-check-syntax doc)))
(define (doc-checked? doc)
(Doc-checked? doc))
(define (new-doc uri text)
(define doc-text (new lsp-editor%))
(send doc-text insert text 0)
;; the init trace should not be #f
(define doc-trace (new build-trace% [src (uri->path uri)] [doc-text doc-text] [indenter #f]))
(define doc (Doc doc-text doc-trace uri #f #f))
(lazy-check-syntax doc)
doc)
(define (doc-reset! doc new-text)
(define doc-text (Doc-text doc))
(define doc-trace (Doc-trace doc))
(send doc-text erase)
(send doc-trace reset)
(send doc-text insert new-text 0)
(lazy-check-syntax doc))
(define (doc-update! doc st-ln st-ch ed-ln ed-ch text)
(define doc-text (Doc-text doc))
(define doc-trace (Doc-trace doc))
(define st-pos (doc-pos doc st-ln st-ch))
(define end-pos (doc-pos doc ed-ln ed-ch))
(define old-len (- end-pos st-pos))
(define new-len (string-length text))
;; try reuse old information as the check-syntax can fail
;; and return the old build-trace% object
(cond [(> new-len old-len) (send doc-trace expand end-pos (+ st-pos new-len))]
[(< new-len old-len) (send doc-trace contract (+ st-pos new-len) end-pos)])
(send doc-text replace text st-pos end-pos)
(lazy-check-syntax doc))
(define-syntax-rule (doc-batch-change doc expr ...)
(let ()
(set-Doc-during-batch-change?! doc #t)
expr ...
(set-Doc-during-batch-change?! doc #f)
(lazy-check-syntax doc)))
(define (doc-pos doc line ch)
(send (Doc-text doc) line/char->pos line ch))
(define (doc-line/ch doc pos)
(match-define (list line char) (send (Doc-text doc) pos->line/char pos))
(values line char))
(define (doc-line-start-pos doc line)
(send (Doc-text doc) line-start-pos line))
(define (doc-line-end-pos doc line)
(send (Doc-text doc) line-end-pos line))
(define (doc-endpos doc)
(send (Doc-text doc) end-pos))
(define (doc-find-containing-paren doc pos)
(define text (send (Doc-text doc) get-text))
(define l (string-length text))
(cond
[(>= pos l) #f]
[else
(let loop ([i pos] [p 0])
(cond
[(< i 0) #f]
[(or (char=? (string-ref text i) #\() (char=? (string-ref text i) #\[))
(if (> p 0) (loop (- i 1) (- p 1)) i)]
[(or (char=? (string-ref text i) #\)) (char=? (string-ref text i) #\]))
(loop (- i 1) (+ p 1))]
[else (loop (- i 1) p)]))]))
(define (get-symbols doc-text)
(define text (send doc-text get-text))
(define in (open-input-string text))
(port-count-lines! in)
(define lexer (get-lexer in))
(define symbols (make-interval-map))
(for ([lst (in-port (lexer-wrap lexer) in)]
#:when (set-member? '(constant string symbol) (first (rest lst))))
(match-define (list text type paren? start end) lst)
(interval-map-set! symbols start end (list text type)))
symbols)
;; Wrapper for in-port, returns a list or EOF.
(define ((lexer-wrap lexer) in)
(define (eof-or-list txt type paren? start end)
(if (eof-object? txt)
eof
(list txt type paren? start end)))
(cond
[(procedure? lexer)
(define-values (txt type paren? start end)
(lexer in))
(eof-or-list txt type paren? start end)]
[(cons? lexer)
(define-values (txt type paren? start end backup mode)
((car lexer) in 0 (cdr lexer)))
(set! lexer (cons (car lexer) mode))
(eof-or-list txt type paren? start end)]))
;; Call module-lexer on an input port, then discard all
;; values except the lexer.
(define (get-lexer in)
(match-define-values
(_ _ _ _ _ _ lexer)
(module-lexer in 0 #f))
(cond
[(procedure? lexer) lexer]
[(cons? lexer) lexer]
[(eq? lexer 'no-lang-line) racket-lexer]
[(eq? lexer 'before-lang-line) racket-lexer]
[else racket-lexer]))
(define (doc-get-symbols doc)
(get-symbols (Doc-text doc)))
;; definition BEG ;;
(define (get-def path doc-text id)
(define collector
(new (class (annotations-mixin object%)
(define defs (make-hash))
(define/public (get id) (hash-ref defs id #f))
(define/override (syncheck:add-definition-target source-obj start end id mods)
(hash-set! defs id (cons start end)))
(super-new))))
(define-values (src-dir _file _dir?)
(split-path path))
(define in (open-input-string (send doc-text get-text)))
(define ns (make-base-namespace))
(define-values (add-syntax done)
(make-traversal ns src-dir))
(parameterize ([current-annotations collector]
[current-namespace ns]
[current-load-relative-directory src-dir])
(define stx (expand (with-module-reading-parameterization
(λ () (read-syntax path in)))))
(add-syntax stx))
(send collector get id))
(define (get-definition-by-id path id)
(define doc-text (new lsp-editor%))
(send doc-text load-file path)
(match-define (cons start end) (get-def path doc-text id))
(match-define (list st-ln st-ch) (send doc-text pos->line/char start))
(match-define (list ed-ln ed-ch) (send doc-text pos->line/char end))
(make-Range #:start (make-Position #:line st-ln #:character st-ch)
#:end (make-Position #:line ed-ln #:character ed-ch)))
;; definition END ;;
;; formatting ;;
;; Shared path for all formatting requests
(define (format! this-doc st-ln st-ch ed-ln ed-ch
#:on-type? [on-type? #f]
#:formatting-options opts)
(define doc-text (Doc-text this-doc))
(define doc-trace (Doc-trace this-doc))
(define indenter (send doc-trace get-indenter))
(define start-pos (doc-pos this-doc st-ln st-ch))
;; Adjust for line endings (#92)
(define end-pos (max start-pos (sub1 (doc-pos this-doc ed-ln ed-ch))))
(define start-line (send doc-text at-line start-pos))
(define end-line (send doc-text at-line end-pos))
(define mut-doc-text (send doc-text copy))
;; replace \t with spaces at line `(sub1 start-line)`
;; as we cannot make `compute-racket-amount-to-indent`
;; to respect the given tab size
(replace-tab! mut-doc-text
(max 0 (sub1 start-line))
(FormattingOptions-tab-size opts))
(define indenter-wp (indenter-wrapper indenter mut-doc-text on-type?))
(define skip-this-line? #f)
(if (eq? indenter 'missing) (json-null)
(let loop ([line start-line])
(define line-start (send mut-doc-text line-start-pos line))
(define line-end (send mut-doc-text line-end-pos line))
(for ([i (range line-start (add1 line-end))])
(when (and (char=? #\" (send mut-doc-text get-char i))
(not (char=? #\\ (send mut-doc-text get-char (sub1 i)))))
(set! skip-this-line? (not skip-this-line?))))
(if (> line end-line)
null
(append (filter-map
values
;; NOTE: The order is important here.
;; `remove-trailing-space!` deletes content relative to the initial document
;; position. If we were to instead call `indent-line!` first and then
;; `remove-trailing-space!` second, the remove step could result in
;; losing user entered code.
(list (if (false? (FormattingOptions-trim-trailing-whitespace opts))
#f
(remove-trailing-space! mut-doc-text skip-this-line? line))
(indent-line! mut-doc-text indenter-wp line)))
(loop (add1 line)))))))
(define (replace-tab! doc-text line tabsize)
(define old-line (send doc-text get-line line))
(define spaces (make-string tabsize #\space))
(define new-line-str (string-replace old-line "\t" spaces))
(send doc-text replace-in-line
new-line-str
line 0 (string-length old-line)))
(define (indenter-wrapper indenter doc-text on-type?)
(λ (line)
(cond [(and (not on-type?)
(= (send doc-text line-start-pos line)
(send doc-text line-end-pos line)))
#f]
[else
(define line-start (send doc-text line-start-pos line))
(if indenter
(or (send doc-text run-indenter indenter line-start)
(send doc-text compute-racket-amount-to-indent line-start))
(send doc-text compute-racket-amount-to-indent line-start))])))
;; Returns a TextEdit, or #f if the line is a part of multiple-line string
(define (remove-trailing-space! doc-text in-string? line)
(define line-text (send doc-text get-line line))
(cond
[(not in-string?)
(define from (string-length (string-trim line-text #px"\\s+" #:left? #f)))
(define to (string-length line-text))
(send doc-text replace-in-line "" line from to)
(TextEdit #:range (Range #:start (Pos #:line line #:char from)
#:end (Pos #:line line #:char to))
#:newText "")]
[else #f]))
(define (extract-indent-string content)
(define len
(or (for/first ([(c i) (in-indexed content)]
#:when (not (char-whitespace? c)))
i)
(string-length content)))
(substring content 0 len))
;; Returns a TextEdit, or #f if the line is already correct.
(define (indent-line! doc-text indenter line)
(define content (send doc-text get-line line))
(define old-indent-string (extract-indent-string content))
(define expect-indent (indenter line))
(define really-indent (string-length old-indent-string))
(define has-tab? (string-contains? old-indent-string "\t"))
(cond [(false? expect-indent) #f]
[(and (= expect-indent really-indent) (not has-tab?)) #f]
[else
(define new-text (make-string expect-indent #\space))
(send doc-text replace-in-line new-text line 0 really-indent)
(TextEdit #:range (Range #:start (Pos #:line line #:char 0)
#:end (Pos #:line line #:char really-indent))
#:newText new-text)]))
(define (token-type-encoding token)
(index-of *semantic-token-types* (SemanticToken-type token)))
(define (token-modifier-encoding token)
(define indexes (indexes-where *semantic-token-modifiers*
(λ (m) (memq m (SemanticToken-modifiers token)))))
;; build a bit flag of the modifiers of `token`.
;;
;; equivalent to C family pseudocode
;;
;; uint32_t flag = 0
;; for index in indexes:
;; flag = flag | (1 << index)
;; return flag
;;
;; But the integer bit width is ignored here, because
;; the *semantic-token-modifiers* is very small.
(for/sum ([index indexes])
(expt 2 index)))
;; encode `token` using relative encoding
;;
;; each token is encoded as five integers (copied from lsp specificatioin 3.17):
;; * deltaLine: token line number, relative to the start of the previous token
;; * deltaStart: token start character, relative to the start of the previous token
;; (relative to 0 or the previous token’s start if they are on the same line)
;; * length: the length of the token.
;; * tokenType: will be looked up in SemanticTokensLegend.tokenTypes.
;; We currently ask that tokenType < 65536.
;; * tokenModifiers: each set bit will be looked up in SemanticTokensLegend.tokenModifiers
;;
;; for the first token, its previous token is defined as a zero length fake token which
;; has line number 0 and character position 0.
(define (token-encoding editor token prev-pos)
(match-define (list line ch) (send editor pos->line/char (SemanticToken-start token)))
(match-define (list prev-line prev-ch) (send editor pos->line/char prev-pos))
(define delta-line (- line prev-line))
(define delta-start
(if (= line prev-line)
(- ch prev-ch)
ch))
(define len (- (SemanticToken-end token) (SemanticToken-start token)))
(define type (token-type-encoding token))
(define modifier (token-modifier-encoding token))
(values delta-line delta-start len type modifier))
;; get the tokens whose range are contained in interval [pos-start, pos-end)
;; the tokens whose range intersects the given range is included.
;; the previous token of the first token in the result is defined as a zero length fake token which
;; has line number 0 and character position 0.
(define (doc-range-tokens editor uri pos-start pos-end)
(define tokens (collect-semantic-tokens editor (uri->path uri)))
(define tokens-in-range
(filter-not (λ (tok) (or (<= (SemanticToken-end tok) pos-start)
(>= (SemanticToken-start tok) pos-end)))
tokens))
(for/fold ([result '()]
[prev-pos 0]
#:result (flatten (reverse result)))
([token tokens-in-range])
(define-values (delta-line delta-start len type modifier)
(token-encoding editor token prev-pos))
(values (cons (list delta-line delta-start len type modifier) result)
(SemanticToken-start token))))
(provide Doc-text
Doc-trace
new-doc
doc-checked?
doc-update!
doc-reset!
doc-batch-change
doc-pos
doc-endpos
doc-line/ch
doc-line-start-pos
doc-line-end-pos
doc-find-containing-paren
doc-get-symbols
get-definition-by-id
format!
doc-range-tokens)