@@ -12,7 +12,10 @@ import {
1212 TextDocumentSyncKind ,
1313 InitializeResult ,
1414 Hover ,
15- MarkupKind
15+ MarkupKind ,
16+ DocumentSymbol ,
17+ SymbolKind ,
18+ Range
1619} from 'vscode-languageserver/node' ;
1720
1821import {
@@ -131,6 +134,15 @@ connection.onInitialize((params: InitializeParams) => {
131134 return result ;
132135} ) ;
133136
137+ connection . onDocumentSymbol ( params => {
138+ const document = documents . get ( params . textDocument . uri ) ;
139+ if ( ! document ) {
140+ return [ ] ;
141+ }
142+
143+ return parseDocumentSymbols ( document ) ;
144+ } ) ;
145+
134146connection . onInitialized ( ( ) => {
135147 if ( hasConfigurationCapability ) {
136148 // Register for all configuration changes.
@@ -455,4 +467,99 @@ connection.onHover(
455467documents . listen ( connection ) ;
456468
457469// Listen on the connection
458- connection . listen ( ) ;
470+ connection . listen ( ) ;
471+
472+ interface SymbolMatch {
473+ name : string ;
474+ detail : string ;
475+ kind : SymbolKind ;
476+ }
477+
478+ function matchSymbol ( line : string ) : SymbolMatch | undefined {
479+ const defMatch = line . match ( / ^ d e f \s + ( [ \w \. ] + ) \s * \( ( [ ^ ) ] * ) \) \s * : ? / ) ;
480+ if ( defMatch ) {
481+ const [ , name , params ] = defMatch ;
482+ return {
483+ name,
484+ detail : `(${ params . trim ( ) } )` ,
485+ kind : SymbolKind . Function
486+ } ;
487+ }
488+
489+ const classMatch = line . match ( / ^ c l a s s \s + ( [ \w \. ] + ) \s * ( \( [ ^ ) ] * \) ) ? \s * : ? / ) ;
490+ if ( classMatch ) {
491+ const [ , name , bases = '' ] = classMatch ;
492+ return {
493+ name,
494+ detail : bases . trim ( ) ,
495+ kind : SymbolKind . Class
496+ } ;
497+ }
498+
499+ const assignmentMatch = line . match ( / ^ ( [ A - Z a - z _ ] \w * ) \s * = \s * .+ / ) ;
500+ if ( assignmentMatch ) {
501+ const [ , name ] = assignmentMatch ;
502+ return {
503+ name,
504+ detail : 'assignment' ,
505+ kind : SymbolKind . Variable
506+ } ;
507+ }
508+
509+ return undefined ;
510+ }
511+
512+ function buildRange ( line : number , indent : number , length : number ) : Range {
513+ return {
514+ start : { line, character : indent } ,
515+ end : { line, character : length }
516+ } ;
517+ }
518+
519+ function parseDocumentSymbols ( textDocument : TextDocument ) : DocumentSymbol [ ] {
520+ const rootSymbols : DocumentSymbol [ ] = [ ] ;
521+ const stack : Array < { indent : number ; symbol : DocumentSymbol } > = [ ] ;
522+
523+ for ( let line = 0 ; line < textDocument . lineCount ; line ++ ) {
524+ const lineText = textDocument . getText ( {
525+ start : { line, character : 0 } ,
526+ end : { line : line + 1 , character : 0 }
527+ } ) ;
528+ const content = lineText . replace ( / \n $ / , '' ) ;
529+ const trimmed = content . trim ( ) ;
530+
531+ if ( ! trimmed || trimmed . startsWith ( '#' ) ) {
532+ continue ;
533+ }
534+
535+ const symbolMatch = matchSymbol ( trimmed ) ;
536+ if ( ! symbolMatch ) {
537+ continue ;
538+ }
539+
540+ const indent = content . length - content . trimStart ( ) . length ;
541+ const range = buildRange ( line , indent , content . length ) ;
542+ const symbol : DocumentSymbol = {
543+ name : symbolMatch . name ,
544+ detail : symbolMatch . detail ,
545+ kind : symbolMatch . kind ,
546+ range,
547+ selectionRange : range ,
548+ children : [ ]
549+ } ;
550+
551+ while ( stack . length && indent <= stack [ stack . length - 1 ] . indent ) {
552+ stack . pop ( ) ;
553+ }
554+
555+ if ( stack . length ) {
556+ stack [ stack . length - 1 ] . symbol . children ?. push ( symbol ) ;
557+ } else {
558+ rootSymbols . push ( symbol ) ;
559+ }
560+
561+ stack . push ( { indent, symbol } ) ;
562+ }
563+
564+ return rootSymbols ;
565+ }
0 commit comments