@@ -5,11 +5,13 @@ use lsp_server::{Connection, Message, Notification, Request, Response};
55use lsp_types:: {
66 Diagnostic , DiagnosticSeverity , DidChangeTextDocumentParams , DidCloseTextDocumentParams ,
77 DidOpenTextDocumentParams , DidSaveTextDocumentParams , DocumentFormattingParams ,
8- InitializeParams , InitializeResult , InitializedParams , Position , PublishDiagnosticsParams ,
9- Range , ServerCapabilities , TextDocumentSyncCapability , TextDocumentSyncKind , TextEdit , Uri ,
8+ DocumentSymbolParams , DocumentSymbolResponse , InitializedParams , Location , Position ,
9+ PublishDiagnosticsParams , Range , SymbolInformation , SymbolKind , TextEdit , Uri ,
10+ WorkspaceSymbolParams ,
1011} ;
1112use serde_json:: { from_value, to_value, Value } ;
1213use technique:: formatting:: Identity ;
14+ use technique:: language:: { Document , Technique } ;
1315use tracing:: { debug, error, info, warn} ;
1416
1517use crate :: formatting;
@@ -99,6 +101,40 @@ impl TechniqueLanguageServer {
99101 }
100102 }
101103 }
104+ "textDocument/documentSymbol" => {
105+ let params: DocumentSymbolParams = from_value ( req. params ) ?;
106+ match self . handle_document_symbol ( params) {
107+ Ok ( result) => {
108+ let response = Response :: new_ok ( req. id , result) ;
109+ sender ( Message :: Response ( response) ) ?;
110+ }
111+ Err ( err) => {
112+ let response = Response :: new_err (
113+ req. id ,
114+ lsp_server:: ErrorCode :: InternalError as i32 ,
115+ err. to_string ( ) ,
116+ ) ;
117+ sender ( Message :: Response ( response) ) ?;
118+ }
119+ }
120+ }
121+ "workspace/symbol" => {
122+ let params: WorkspaceSymbolParams = from_value ( req. params ) ?;
123+ match self . handle_workspace_symbol ( params) {
124+ Ok ( result) => {
125+ let response = Response :: new_ok ( req. id , result) ;
126+ sender ( Message :: Response ( response) ) ?;
127+ }
128+ Err ( err) => {
129+ let response = Response :: new_err (
130+ req. id ,
131+ lsp_server:: ErrorCode :: InternalError as i32 ,
132+ err. to_string ( ) ,
133+ ) ;
134+ sender ( Message :: Response ( response) ) ?;
135+ }
136+ }
137+ }
102138 "shutdown" => {
103139 info ! ( "Language Server received shutdown request" ) ;
104140 let response = Response :: new_ok ( req. id , Value :: Null ) ;
@@ -316,6 +352,131 @@ impl TechniqueLanguageServer {
316352 Ok ( Some ( vec ! [ edit] ) )
317353 }
318354
355+ fn handle_document_symbol (
356+ & self ,
357+ params : DocumentSymbolParams ,
358+ ) -> Result < DocumentSymbolResponse , Box < dyn std:: error:: Error + Sync + Send > > {
359+ let uri = params
360+ . text_document
361+ . uri ;
362+
363+ debug ! ( "Document symbol request: {:?}" , uri) ;
364+
365+ // Get content from our documents map
366+ let content = match self
367+ . documents
368+ . get ( & uri)
369+ {
370+ Some ( content) => content,
371+ None => {
372+ return Ok ( DocumentSymbolResponse :: Flat ( vec ! [ ] ) ) ;
373+ }
374+ } ;
375+
376+ let path = Path :: new (
377+ uri. path ( )
378+ . as_str ( ) ,
379+ ) ;
380+
381+ // Parse document with recovery to get symbols even if there are errors
382+ let document = match parsing:: parse_with_recovery ( path, content) {
383+ Ok ( document) => document,
384+ Err ( _) => {
385+ // Return empty symbols if parsing fails completely
386+ return Ok ( DocumentSymbolResponse :: Flat ( vec ! [ ] ) ) ;
387+ }
388+ } ;
389+
390+ let symbols = self . extract_symbols_from_document ( & uri, content, & document) ;
391+ Ok ( DocumentSymbolResponse :: Flat ( symbols) )
392+ }
393+
394+ fn handle_workspace_symbol (
395+ & self ,
396+ params : WorkspaceSymbolParams ,
397+ ) -> Result < Option < Vec < SymbolInformation > > , Box < dyn std:: error:: Error + Sync + Send > > {
398+ let query = params
399+ . query
400+ . to_lowercase ( ) ;
401+ debug ! ( "Workspace symbol request: query={:?}" , query) ;
402+
403+ let mut all_symbols = Vec :: new ( ) ;
404+
405+ // Search through all open documents
406+ for ( uri, content) in & self . documents {
407+ let path = Path :: new (
408+ uri. path ( )
409+ . as_str ( ) ,
410+ ) ;
411+
412+ // Try to parse each document
413+ if let Ok ( document) = parsing:: parse_with_recovery ( & path, content) {
414+ let symbols = self . extract_symbols_from_document ( uri, content, & document) ;
415+
416+ // Filter symbols by query
417+ for symbol in symbols {
418+ if query. is_empty ( )
419+ || symbol
420+ . name
421+ . to_lowercase ( )
422+ . contains ( & query)
423+ {
424+ all_symbols. push ( symbol) ;
425+ }
426+ }
427+ }
428+ }
429+
430+ Ok ( Some ( all_symbols) )
431+ }
432+
433+ fn extract_symbols_from_document (
434+ & self ,
435+ uri : & Uri ,
436+ content : & str ,
437+ document : & Document ,
438+ ) -> Vec < SymbolInformation > {
439+ let mut symbols = Vec :: new ( ) ;
440+
441+ if let Some ( ref body) = document. body {
442+ match body {
443+ Technique :: Procedures ( procedures) => {
444+ for procedure in procedures {
445+ let name = procedure
446+ . name
447+ . 0 ;
448+
449+ // Calculate the byte offset of the name using pointer arithmetic
450+ let offset = calculate_slice_offset ( content, name) . unwrap_or ( 0 ) ;
451+ let position = offset_to_position ( content, offset) ;
452+
453+ #[ allow( deprecated) ]
454+ let symbol = SymbolInformation {
455+ name : name. to_string ( ) ,
456+ kind : SymbolKind :: CONSTRUCTOR ,
457+ tags : None ,
458+ deprecated : None , // deprecated but still required, how annoying
459+ location : Location {
460+ uri : uri. clone ( ) ,
461+ range : Range {
462+ start : position,
463+ end : position,
464+ } ,
465+ } ,
466+ container_name : None ,
467+ } ;
468+ symbols. push ( symbol) ;
469+ }
470+ }
471+ _ => {
472+ // Steps or Empty - no symbols to extract
473+ }
474+ }
475+ }
476+
477+ symbols
478+ }
479+
319480 /// Parse document and convert errors to diagnostics
320481 fn parse_and_report < E > (
321482 & self ,
@@ -531,6 +692,23 @@ impl TechniqueLanguageServer {
531692 }
532693}
533694
695+ /// Calculate the byte offset of a substring within a parent string using
696+ /// pointer arithmetic.
697+ ///
698+ /// Returns None if the substring is not actually part of the parent string,
699+ /// checking first to see if the substring pointer is actually within the
700+ /// bounds of the parent string.
701+ fn calculate_slice_offset ( parent : & str , substring : & str ) -> Option < usize > {
702+ let parent_ptr = parent. as_ptr ( ) as usize ;
703+ let substring_ptr = substring. as_ptr ( ) as usize ;
704+
705+ if substring_ptr >= parent_ptr && substring_ptr < parent_ptr + parent. len ( ) {
706+ Some ( substring_ptr - parent_ptr)
707+ } else {
708+ None
709+ }
710+ }
711+
534712/// Convert byte offset to LSP Position
535713fn offset_to_position ( text : & str , offset : usize ) -> Position {
536714 let line = calculate_line_number ( text, offset) as u32 ;
@@ -542,6 +720,27 @@ fn offset_to_position(text: &str, offset: usize) -> Position {
542720mod tests {
543721 use super :: * ;
544722
723+ #[ test]
724+ fn test_calculate_str_offsets ( ) {
725+ let parent = "hello world, this is a test" ;
726+
727+ // Test substring that is part of parent
728+ let substring = & parent[ 6 ..11 ] ; // "world"
729+ assert_eq ! ( calculate_slice_offset( parent, substring) , Some ( 6 ) ) ;
730+
731+ // Test substring at the beginning
732+ let substring = & parent[ 0 ..5 ] ; // "hello"
733+ assert_eq ! ( calculate_slice_offset( parent, substring) , Some ( 0 ) ) ;
734+
735+ // Test substring at the end
736+ let substring = & parent[ 23 ..27 ] ; // "test"
737+ assert_eq ! ( calculate_slice_offset( parent, substring) , Some ( 23 ) ) ;
738+
739+ // Test substring that is not part of parent
740+ let other = "not from parent" ;
741+ assert_eq ! ( calculate_slice_offset( parent, other) , None ) ;
742+ }
743+
545744 #[ test]
546745 fn test_offset_to_position ( ) {
547746 let text = "line 1\n line 2\n line 3" ;
0 commit comments