Skip to content

Commit 1d2c32b

Browse files
authored
sticky scroll for #lang racket (#215)
resolve #180
1 parent ef09f1e commit 1d2c32b

10 files changed

Lines changed: 503 additions & 9 deletions

File tree

common/interfaces.rkt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
(json-type-out DocumentHighlight)
2929
(json-type-out SymbolKind)
3030
(json-type-out SymbolInformation)
31+
(json-type-out DocumentSymbol)
3132
(json-type-out Hover)
3233
(json-type-out SignatureInformation)
3334
(json-type-out SignatureHelp)
@@ -157,6 +158,17 @@
157158
[kind SymbolKind]
158159
[location Location])
159160

161+
;; Hierarchical document symbol. `range` covers the whole form (including its
162+
;; body) while `selectionRange` only covers the name. `children` is a list of
163+
;; DocumentSymbol, typed as list? because define-json-struct cannot express
164+
;; self-referential field types; encoding still recurses via ->jsexpr.
165+
(define-json-struct DocumentSymbol
166+
[name string?]
167+
[kind SymbolKind]
168+
[range Range]
169+
[selectionRange Range]
170+
[children list?])
171+
160172
(define-json-struct Hover
161173
[contents string?]
162174
[range Range])

doclib/doc.rkt

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@
1515
"lexer.rkt"
1616
(only-in "lexer/state.rkt"
1717
lexer-state-body-forest)
18+
(only-in "lexer/token-tree.rkt"
19+
Token-Leaf? Token-Leaf-span
20+
Token-List? Token-List-children
21+
Token-Prefix-Tree?
22+
Token-Forest-nodes
23+
token-node-children
24+
token-node-start
25+
token-node-end
26+
non-skippable-node?
27+
token-leaf-type?)
1828
"doc-lang.rkt"
1929
racket/match
2030
racket/contract
@@ -795,6 +805,184 @@
795805
#:location (Location #:uri uri
796806
#:range range))))
797807

808+
;; Hierarchical Document Symbols: returns a tree of DocumentSymbol.
809+
;;
810+
;; The tree is built by recursing over the lexer's token forest, which already
811+
;; encodes paren nesting. Definition-like forms (define, struct, module+, ...)
812+
;; become symbols whose `range` spans the whole form and whose `selectionRange`
813+
;; covers the defined name; any other form is transparent and passes nested
814+
;; definitions up to the enclosing symbol. The forest tolerates incomplete code
815+
;; (forms left open mid-edit still parse to the end of the document), so the
816+
;; symbol tree stays stable during editing.
817+
818+
;; Maps the head symbol of a form to how its SymbolKind is chosen. 'fn-or-var
819+
;; picks Function when the defined name uses the function shorthand
820+
;; (define (f x) ...) and Variable otherwise.
821+
(define *definition-form-kinds*
822+
(hash "define" 'fn-or-var
823+
"define/contract" 'fn-or-var
824+
"define/public" 'fn-or-var
825+
"define/private" 'fn-or-var
826+
"define/override" 'fn-or-var
827+
"define/augment" 'fn-or-var
828+
"define-values" SymbolKind-Variable
829+
"define-syntax" 'fn-or-var
830+
"define-syntax-rule" SymbolKind-Function
831+
"define-syntaxes" SymbolKind-Variable
832+
"define-match-expander" SymbolKind-Function
833+
"define-struct" SymbolKind-Struct
834+
"struct" SymbolKind-Struct
835+
"module" SymbolKind-Module
836+
"module+" SymbolKind-Module
837+
"module*" SymbolKind-Module))
838+
839+
;; Container forms become a symbol named after their own head so that
840+
;; scrolling inside a long body keeps a sticky header even though the form
841+
;; defines no name itself.
842+
(define *container-form-kinds*
843+
(hash "provide" SymbolKind-Namespace
844+
"require" SymbolKind-Namespace
845+
"class" SymbolKind-Class
846+
"class*" SymbolKind-Class
847+
"match" SymbolKind-Object
848+
"match*" SymbolKind-Object
849+
"cond" SymbolKind-Object
850+
"for" SymbolKind-Object
851+
"for/list" SymbolKind-Object
852+
"for/vector" SymbolKind-Object
853+
"for/hash" SymbolKind-Object
854+
"for/hasheq" SymbolKind-Object
855+
"for/hasheqv" SymbolKind-Object
856+
"for/hashalw" SymbolKind-Object
857+
"for/and" SymbolKind-Object
858+
"for/or" SymbolKind-Object
859+
"for/sum" SymbolKind-Object
860+
"for/product" SymbolKind-Object
861+
"for/lists" SymbolKind-Object
862+
"for/first" SymbolKind-Object
863+
"for/last" SymbolKind-Object
864+
"for/fold" SymbolKind-Object
865+
"for/foldr" SymbolKind-Object))
866+
867+
;; Class member forms: every name they declare becomes a Field symbol, either
868+
;; a bare name as in (field z) or a binding group as in (field [y 0]).
869+
(define *field-form-heads*
870+
(set "field" "init-field" "init"))
871+
872+
(define/contract (doc-symbols-hierarchical doc)
873+
(-> Doc? (listof DocumentSymbol?))
874+
;; Paren tracking means nothing in non-sexp languages (e.g. Rhombus,
875+
;; Scribble): form extents are set by blocks or markup rather than parens,
876+
;; so any symbol the walker produced would have a bogus range. Report no
877+
;; symbols instead. Unknown languages keep the sexp treatment, matching the
878+
;; lexer fallback used elsewhere.
879+
(if (eq? (Language-Info-body-mode (doc-language-info doc)) 'non-sexp)
880+
'()
881+
(doc-symbols-hierarchical/sexp doc)))
882+
883+
;; Walk the lexer's token forest. Each `(...)` form is a Token-List whose head
884+
;; is its first meaningful child. A definition form becomes a symbol named after
885+
;; the bound name, a container form a symbol named after its own head, and a
886+
;; field form one Field symbol per declared name. Any other form is transparent:
887+
;; it contributes its nested definitions to the enclosing scope. The forest
888+
;; already tracks paren nesting and tolerates unclosed mid-edit forms, so this
889+
;; reuses that structure instead of re-deriving it from the flat token stream.
890+
(define (doc-symbols-hierarchical/sexp doc)
891+
(define text (LexerSnapshot-text (doc-lexer-snapshot doc)))
892+
(define (span-text span)
893+
(substring text (LexerTokenSpan-start span) (LexerTokenSpan-end span)))
894+
895+
(define (symbol-leaf? node)
896+
(and (Token-Leaf? node)
897+
(token-leaf-type? node 'symbol)))
898+
899+
(define (meaningful nodes)
900+
(filter non-skippable-node? nodes))
901+
902+
;; The first symbol leaf in pre-order across `nodes`, descending into lists
903+
;; and prefix forms. This is the bound name of a definition, e.g. `f` in
904+
;; `(define (f x) ...)` or `a` in `(define-values (a b) ...)`.
905+
(define (first-symbol-span nodes)
906+
(for/or ([node (in-list (meaningful nodes))])
907+
(cond
908+
[(symbol-leaf? node) (Token-Leaf-span node)]
909+
[else (first-symbol-span (token-node-children node))])))
910+
911+
(define (node->document-symbol node name-span kind children)
912+
(DocumentSymbol
913+
#:name (span-text name-span)
914+
#:kind kind
915+
#:range (abs-range->range doc
916+
(token-node-start node)
917+
(token-node-end node))
918+
#:selectionRange (abs-range->range doc
919+
(LexerTokenSpan-start name-span)
920+
(LexerTokenSpan-end name-span))
921+
#:children children))
922+
923+
(define (definition-symbol-kind head function?)
924+
(match (hash-ref *definition-form-kinds* head)
925+
['fn-or-var (if function? SymbolKind-Function SymbolKind-Variable)]
926+
[kind kind]))
927+
928+
(define (collect-symbols nodes)
929+
(append-map (lambda (node)
930+
;; Symbols contributed by `node` to its enclosing scope.
931+
(cond
932+
[(Token-List? node) (list-symbols node)]
933+
;; A quoted/prefixed datum is transparent to symbol collection.
934+
[(Token-Prefix-Tree? node) (collect-symbols (token-node-children node))]
935+
[else '()]))
936+
(meaningful nodes)))
937+
938+
(define (list-symbols node)
939+
(define forms (meaningful (Token-List-children node)))
940+
(define head-node (and (pair? forms) (car forms)))
941+
(define head (and head-node
942+
(symbol-leaf? head-node)
943+
(span-text (Token-Leaf-span head-node))))
944+
(define args (if (pair? forms) (cdr forms) '()))
945+
(cond
946+
[(and head (set-member? *field-form-heads* head)
947+
;; A field form like `(field z)`, `(field [y 0])`, or a mix declares one Field
948+
;; per argument: a bare name covers just the name, a binding group covers the
949+
;; whole `[name v]`.
950+
(filter-map
951+
(lambda (node)
952+
(define name-span
953+
(cond
954+
[(symbol-leaf? node) (Token-Leaf-span node)]
955+
[(Token-List? node) (first-symbol-span (Token-List-children node))]
956+
[else #f]))
957+
(and name-span
958+
(node->document-symbol node name-span SymbolKind-Field '())))
959+
args))]
960+
;; A container form is its own name, so scrolling its body keeps a sticky
961+
;; header even though the form binds no name itself.
962+
[(and head (hash-has-key? *container-form-kinds* head))
963+
(list (node->document-symbol node
964+
(Token-Leaf-span head-node)
965+
(hash-ref *container-form-kinds* head)
966+
(collect-symbols args)))]
967+
[(and head (hash-has-key? *definition-form-kinds* head))
968+
(define name-span (first-symbol-span args))
969+
(cond
970+
[name-span
971+
;; The function shorthand `(define (f x) ...)` puts the name inside a
972+
;; nested list, so the first argument is a list rather than a symbol.
973+
(define function? (and (pair? args) (not (symbol-leaf? (car args)))))
974+
(list (node->document-symbol node
975+
name-span
976+
(definition-symbol-kind head function?)
977+
;; The first argument is the name header;
978+
;; nested definitions live in the body.
979+
(collect-symbols (cdr args))))]
980+
;; A definition head with no name yet (mid-edit) is transparent.
981+
[else (collect-symbols args)])]
982+
[else (collect-symbols args)]))
983+
984+
(collect-symbols (Token-Forest-nodes (doc-body-forest doc))))
985+
798986
(provide Doc?
799987
Doc-version
800988
Doc-uri
@@ -844,4 +1032,5 @@
8441032
doc-rename
8451033
doc-prepare-rename
8461034
doc-symbols
847-
)
1035+
doc-symbols-hierarchical)
1036+

lsp/methods.rkt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@
240240
(jsexpr-ref capabilities '(workspace configuration))
241241
#f))
242242

243+
(text-document/client-capability-hierarchical-document-symbol?
244+
(if (jsexpr-has-key? capabilities '(textDocument documentSymbol hierarchicalDocumentSymbolSupport))
245+
(jsexpr-ref capabilities '(textDocument documentSymbol hierarchicalDocumentSymbolSupport))
246+
#f))
247+
243248
;; If both `rootPath` and `rootUri` are set, then `rootUri` wins.
244249
;; null is there are no folder is open.
245250
(cond

lsp/text-document.rkt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
(success-response id (if (false? result) (json-null) (->jsexpr result))))
1919

2020
(define client-capability-workspace/configuration? (make-parameter #f))
21+
(define client-capability-hierarchical-document-symbol? (make-parameter #f))
2122
(define (fetch-configuration request-client uri)
2223
(when (client-capability-workspace/configuration?)
2324
(request-client "workspace/configuration"
@@ -205,7 +206,11 @@
205206
(define safe-doc (lsp-get-doc uri))
206207
(define results
207208
(with-read-doc safe-doc
208-
(λ (doc) (doc-symbols doc uri))))
209+
(λ (doc)
210+
(cond
211+
[(client-capability-hierarchical-document-symbol?)
212+
(doc-symbols-hierarchical doc)]
213+
[else (doc-symbols doc uri)]))))
209214
(success/enc id results)]
210215
[_
211216
(error-response id ErrorCode-InvalidParams "textDocument/documentSymbol failed")]))
@@ -346,5 +351,6 @@
346351
[full-semantic-tokens (exact-nonnegative-integer? jsexpr? . -> . (or/c jsexpr? (-> jsexpr?)))]
347352
[range-semantic-tokens (exact-nonnegative-integer? jsexpr? . -> . (or/c jsexpr? (-> jsexpr?)))])
348353

349-
client-capability-workspace/configuration?)
354+
client-capability-workspace/configuration?
355+
client-capability-hierarchical-document-symbol?)
350356

tests/client.rkt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030

3131
(define-runtime-path rootFolder "./")
3232

33-
(define/contract (with-racket-lsp proc)
34-
(-> (-> any/c any/c) void?)
33+
(define/contract (with-racket-lsp proc #:capabilities [capabilities (hasheq)])
34+
(->* ((-> any/c any/c)) (#:capabilities jsexpr?) void?)
3535
(parameterize ([response-channel (make-async-channel)]
3636
[request-channel (make-async-channel)]
3737
[notification-channel (make-async-channel)])
@@ -63,7 +63,7 @@
6363
(define init-req
6464
(make-request lsp "initialize"
6565
(hasheq 'processId (getpid)
66-
'capabilities (hasheq)
66+
'capabilities capabilities
6767
'rootPath (path->string (normalize-path rootFolder))
6868
'rootUri (string-append "file://" (path->string (normalize-path rootFolder))))))
6969
(client-send lsp init-req)

0 commit comments

Comments
 (0)