From 5731bd150e9a4fb96cbd4f753c59e83e99bad059 Mon Sep 17 00:00:00 2001 From: zen010101 <60574100+zen010101@users.noreply.github.com> Date: Thu, 25 Dec 2025 22:55:44 +0800 Subject: [PATCH 1/2] Add hierarchical DocumentSymbol support (LSP 3.10) Implement dual-mode DocumentSymbol provider supporting both LSP 2.x flat mode (SymbolInformation) and LSP 3.x hierarchical mode (DocumentSymbol with children). Changes: - Add TSymbolMode enum and TSymbolBuilder class for dual-mode building - Detect client hierarchicalDocumentSymbolSupport capability during init - Build hierarchical structure with parent-child relationships - Return DocumentSymbol (with range/children) or SymbolInformation (with location/containerName) based on client capability Benefits: - Better IDE integration (Outline view shows proper nesting in VS Code) - Enables hierarchical navigation and symbol search (Class/Method paths) - Backward compatible with clients not supporting hierarchical mode - Compliant with LSP 3.10+ specification Tested with VS Code and Serena MCP, both working correctly in hierarchical mode with proper symbol nesting. --- src/protocol/LSP.Capabilities.pas | 68 ++++++ src/protocol/LSP.DocumentSymbol.pas | 18 ++ src/serverprotocol/PasLS.General.pas | 8 + src/serverprotocol/PasLS.Symbols.pas | 326 +++++++++++++++++++++++++-- 4 files changed, 402 insertions(+), 18 deletions(-) diff --git a/src/protocol/LSP.Capabilities.pas b/src/protocol/LSP.Capabilities.pas index 0083502..7b259d8 100644 --- a/src/protocol/LSP.Capabilities.pas +++ b/src/protocol/LSP.Capabilities.pas @@ -88,9 +88,30 @@ TWorkspaceServerCapabilities = class(TLSPStreamable) property workspaceFolders: TWorkspaceFoldersServerCapabilities read fWorkspaceFolders write SetWorkspaceFolders; end; + { TDocumentSymbolClientCapabilities } + + TDocumentSymbolClientCapabilities = class(TLSPStreamable) + private + fHierarchicalDocumentSymbolSupport: boolean; + Public + Procedure Assign(Source : TPersistent); override; + published + // The client supports hierarchical document symbols. + property hierarchicalDocumentSymbolSupport: boolean read fHierarchicalDocumentSymbolSupport write fHierarchicalDocumentSymbolSupport; + end; + { TTextDocumentClientCapabilities } TTextDocumentClientCapabilities = class(TLSPStreamable) + private + fDocumentSymbol: TDocumentSymbolClientCapabilities; + procedure SetDocumentSymbol(AValue: TDocumentSymbolClientCapabilities); + Public + constructor Create; override; + destructor Destroy; override; + Procedure Assign(Source : TPersistent); override; + published + property documentSymbol: TDocumentSymbolClientCapabilities read fDocumentSymbol write SetDocumentSymbol; end; { TClientCapabilities } @@ -159,6 +180,53 @@ TServerCapabilities = class(TLSPStreamable) implementation +{ TDocumentSymbolClientCapabilities } + +procedure TDocumentSymbolClientCapabilities.Assign(Source : TPersistent); +var + Src : TDocumentSymbolClientCapabilities absolute Source; +begin + if Source is TDocumentSymbolClientCapabilities then + begin + HierarchicalDocumentSymbolSupport := Src.HierarchicalDocumentSymbolSupport; + end + else + inherited Assign(Source); +end; + +{ TTextDocumentClientCapabilities } + +procedure TTextDocumentClientCapabilities.SetDocumentSymbol( + AValue: TDocumentSymbolClientCapabilities); +begin + if fDocumentSymbol=AValue then Exit; + fDocumentSymbol.Assign(AValue); +end; + +constructor TTextDocumentClientCapabilities.Create; +begin + Inherited; + fDocumentSymbol := TDocumentSymbolClientCapabilities.Create; +end; + +destructor TTextDocumentClientCapabilities.Destroy; +begin + FreeAndNil(fDocumentSymbol); + inherited Destroy; +end; + +procedure TTextDocumentClientCapabilities.Assign(Source : TPersistent); +var + Src : TTextDocumentClientCapabilities absolute Source; +begin + if Source is TTextDocumentClientCapabilities then + begin + DocumentSymbol := Src.DocumentSymbol; + end + else + inherited Assign(Source); +end; + { TWorkspaceClientCapabilities } procedure TWorkspaceClientCapabilities.Assign(Source : TPersistent); diff --git a/src/protocol/LSP.DocumentSymbol.pas b/src/protocol/LSP.DocumentSymbol.pas index bd6fa25..369c654 100644 --- a/src/protocol/LSP.DocumentSymbol.pas +++ b/src/protocol/LSP.DocumentSymbol.pas @@ -150,6 +150,24 @@ TSymbolInformation = class(TCollectionItem) TSymbolInformationItems = specialize TGenericCollection; + { TDocumentSymbolEx } + + { Extended DocumentSymbol with additional fields for pasls-specific needs } + + TSymbolFlag = (sfForwardDeclaration, sfDeprecated); + TSymbolFlags = set of TSymbolFlag; + + TDocumentSymbolEx = class(TDocumentSymbol) + private + fRawJSON: String; + fFlags: TSymbolFlags; + public + property RawJSON: String read fRawJSON write fRawJSON; + property Flags: TSymbolFlags read fFlags write fFlags; + end; + + TDocumentSymbolExItems = specialize TGenericCollection; + { TDocumentSymbolParams } TDocumentSymbolParams = class(TLSPStreamable) diff --git a/src/serverprotocol/PasLS.General.pas b/src/serverprotocol/PasLS.General.pas index 0b4541a..2a192a7 100644 --- a/src/serverprotocol/PasLS.General.pas +++ b/src/serverprotocol/PasLS.General.pas @@ -349,6 +349,14 @@ function TInitialize.Process(var Params : TLSPInitializeParams): TInitializeResu ServerSettings.Assign(Params.initializationOptions); PasLS.Settings.ClientInfo.Assign(Params.ClientInfo); + // Detect hierarchical document symbol support + if Assigned(Params.capabilities) and + Assigned(Params.capabilities.textDocument) and + Assigned(Params.capabilities.textDocument.documentSymbol) then + SetClientCapabilities(Params.capabilities.textDocument.documentSymbol.hierarchicalDocumentSymbolSupport) + else + SetClientCapabilities(false); + // replace macros in server settings Macros.Add('tmpdir', GetTempDir(true)); Macros.Add('root', URIToPath(Params.rootUri)); diff --git a/src/serverprotocol/PasLS.Symbols.pas b/src/serverprotocol/PasLS.Symbols.pas index c0484e0..7be7f77 100644 --- a/src/serverprotocol/PasLS.Symbols.pas +++ b/src/serverprotocol/PasLS.Symbols.pas @@ -78,6 +78,45 @@ TSymbolTableEntry = class property RawJSON: String read GetRawJSON; end; + { TSymbolBuilder } + + { Dual-mode symbol builder supporting both flat (SymbolInformation) + and hierarchical (DocumentSymbol) output } + + TSymbolMode = (smFlat, smHierarchical); + + TSymbolBuilder = class + private + FMode: TSymbolMode; + FEntry: TSymbolTableEntry; + FTool: TCodeTool; + + // For hierarchical mode: map className -> TDocumentSymbolEx + FClassMap: TFPHashObjectList; + FRootSymbols: TDocumentSymbolExItems; + + // For tracking current hierarchy + FCurrentClass: TDocumentSymbolEx; + + function FindOrCreateClass(const AClassName: String; Node: TCodeTreeNode): TDocumentSymbolEx; + procedure SetNodeRange(Symbol: TDocumentSymbolEx; Node: TCodeTreeNode); + public + constructor Create(AEntry: TSymbolTableEntry; ATool: TCodeTool; AMode: TSymbolMode); + destructor Destroy; override; + + // Add symbols based on mode + function AddClass(Node: TCodeTreeNode; const Name: String): TSymbol; + function AddMethod(Node: TCodeTreeNode; const AClassName, AMethodName: String): TSymbol; + function AddGlobalFunction(Node: TCodeTreeNode; const Name: String): TSymbol; + + // Serialization + procedure SerializeSymbols; + + property Mode: TSymbolMode read FMode; + property CurrentClass: TDocumentSymbolEx read FCurrentClass write FCurrentClass; + property RootSymbols: TDocumentSymbolExItems read FRootSymbols; + end; + { TSymbolExtractor } TSymbolExtractor = class @@ -85,6 +124,7 @@ TSymbolExtractor = class Code: TCodeBuffer; Tool: TCodeTool; Entry: TSymbolTableEntry; + Builder: TSymbolBuilder; OverloadMap: TFPHashList; RelatedFiles: TFPHashList; IndentLevel: integer; @@ -182,6 +222,13 @@ TSymbolManager = class var SymbolManager: TSymbolManager = nil; +// Client capabilities storage +var + ClientSupportsHierarchicalSymbols: boolean = false; + +function GetSymbolMode: TSymbolMode; +procedure SetClientCapabilities(SupportsHierarchical: boolean); + implementation uses { RTL } @@ -192,6 +239,19 @@ implementation { Protocol } PasLS.Settings; +function GetSymbolMode: TSymbolMode; +begin + if ClientSupportsHierarchicalSymbols then + Result := smHierarchical + else + Result := smFlat; +end; + +procedure SetClientCapabilities(SupportsHierarchical: boolean); +begin + ClientSupportsHierarchicalSymbols := SupportsHierarchical; +end; + function GetFileKey(Path: String): ShortString; begin result := ExtractFileName(Path); @@ -233,6 +293,202 @@ constructor TSymbol.Create; Create(nil); end; +{ TSymbolBuilder } + +constructor TSymbolBuilder.Create(AEntry: TSymbolTableEntry; ATool: TCodeTool; AMode: TSymbolMode); +begin + FEntry := AEntry; + FTool := ATool; + FMode := AMode; + FCurrentClass := nil; + + if FMode = smHierarchical then + begin + FClassMap := TFPHashObjectList.Create(False); // Don't own objects - they're owned by FRootSymbols + FRootSymbols := TDocumentSymbolExItems.Create; + end; +end; + +destructor TSymbolBuilder.Destroy; +begin + if FMode = smHierarchical then + begin + FreeAndNil(FClassMap); + FreeAndNil(FRootSymbols); + end; + inherited; +end; + +procedure TSymbolBuilder.SetNodeRange(Symbol: TDocumentSymbolEx; Node: TCodeTreeNode); +var + StartPos, EndPos: TCodeXYPosition; +begin + if (FTool = nil) or (Symbol = nil) or (Node = nil) then + Exit; + + FTool.CleanPosToCaret(Node.StartPos, StartPos); + FTool.CleanPosToCaret(Node.EndPos, EndPos); + + Symbol.range.SetRange(StartPos.Y - 1, StartPos.X - 1, EndPos.Y - 1, EndPos.X - 1); + Symbol.selectionRange.SetRange(StartPos.Y - 1, StartPos.X - 1, StartPos.Y - 1, StartPos.X - 1); +end; + +function TSymbolBuilder.FindOrCreateClass(const AClassName: String; Node: TCodeTreeNode): TDocumentSymbolEx; +begin + Result := nil; + + if FMode <> smHierarchical then + Exit; + + // Check if class already exists + Result := TDocumentSymbolEx(FClassMap.Find(AClassName)); + + if Result = nil then + begin + // Create new class symbol in FRootSymbols + Result := FRootSymbols.Add; + Result.name := AClassName; + Result.kind := TSymbolKind._Class; + + // Set ranges using the node + if Node <> nil then + SetNodeRange(Result, Node); + + // Add reference to the FRootSymbols item in class map + // Note: FClassMap doesn't own objects - they're owned by FRootSymbols + FClassMap.Add(AClassName, Result); + end; +end; + +function TSymbolBuilder.AddClass(Node: TCodeTreeNode; const Name: String): TSymbol; +var + CodePos, EndPos: TCodeXYPosition; +begin + case FMode of + smFlat: + begin + // Use existing flat mode: add to Entry.Symbols + if (FTool <> nil) and (Node <> nil) then + begin + FTool.CleanPosToCaret(Node.StartPos, CodePos); + FTool.CleanPosToCaret(Node.EndPos, EndPos); + Result := FEntry.AddSymbol(Name, TSymbolKind._Class, + CodePos.Code.FileName, + CodePos.Y, CodePos.X, + EndPos.Y, EndPos.X); + end + else + Result := nil; + end; + + smHierarchical: + begin + // For hierarchical mode, we don't add duplicate class symbols + // Classes are created on-demand when methods reference them + FCurrentClass := FindOrCreateClass(Name, Node); + Result := nil; // Hierarchical classes are not TSymbol + end; + end; +end; + +function TSymbolBuilder.AddMethod(Node: TCodeTreeNode; const AClassName, AMethodName: String): TSymbol; +var + ClassSymbol: TDocumentSymbolEx; + MethodSymbol: TDocumentSymbolEx; + CodePos, EndPos: TCodeXYPosition; +begin + case FMode of + smFlat: + begin + // Flat mode: add method with containerName + if (FTool <> nil) and (Node <> nil) then + begin + FTool.CleanPosToCaret(Node.StartPos, CodePos); + FTool.CleanPosToCaret(Node.EndPos, EndPos); + Result := FEntry.AddSymbol(AMethodName, TSymbolKind._Function, + CodePos.Code.FileName, + CodePos.Y, CodePos.X, + EndPos.Y, EndPos.X); + if Result <> nil then + Result.containerName := AClassName; + end + else + Result := nil; + end; + + smHierarchical: + begin + // Hierarchical mode: add method to class's children + ClassSymbol := FindOrCreateClass(AClassName, Node); + if ClassSymbol <> nil then + begin + MethodSymbol := TDocumentSymbolEx.Create(ClassSymbol.children); + MethodSymbol.name := AMethodName; + MethodSymbol.kind := TSymbolKind._Function; + SetNodeRange(MethodSymbol, Node); + end; + Result := nil; // Hierarchical symbols are not TSymbol + end; + end; +end; + +function TSymbolBuilder.AddGlobalFunction(Node: TCodeTreeNode; const Name: String): TSymbol; +var + GlobalSymbol: TDocumentSymbolEx; + CodePos, EndPos: TCodeXYPosition; +begin + case FMode of + smFlat: + begin + if (FTool <> nil) and (Node <> nil) then + begin + FTool.CleanPosToCaret(Node.StartPos, CodePos); + FTool.CleanPosToCaret(Node.EndPos, EndPos); + Result := FEntry.AddSymbol(Name, TSymbolKind._Function, + CodePos.Code.FileName, + CodePos.Y, CodePos.X, + EndPos.Y, EndPos.X); + end + else + Result := nil; + end; + + smHierarchical: + begin + // Add to root level (not under any class) + GlobalSymbol := TDocumentSymbolEx.Create(FRootSymbols); + GlobalSymbol.name := Name; + GlobalSymbol.kind := TSymbolKind._Function; + SetNodeRange(GlobalSymbol, Node); + Result := nil; // Hierarchical symbols are not TSymbol + end; + end; +end; + +procedure TSymbolBuilder.SerializeSymbols; +var + SerializedItems: TJSONArray; +begin + case FMode of + smFlat: + begin + // Use existing serialization + FEntry.SerializeSymbols; + end; + + smHierarchical: + begin + // Serialize DocumentSymbol hierarchy + SerializedItems := specialize TLSPStreaming.ToJSON(FRootSymbols) as TJSONArray; + try + FEntry.fRawJSON := SerializedItems.AsJSON; + finally + SerializedItems.Free; + end; + end; + end; +end; + { TSymbolTableEntry } function TSymbolTableEntry.GetRawJSON: String; @@ -468,9 +724,10 @@ procedure TSymbolExtractor.ExtractObjCClassMethods(ClassNode, Node: TCodeTreeNod end; end; -procedure TSymbolExtractor.ExtractTypeDefinition(TypeDefNode, Node: TCodeTreeNode); +procedure TSymbolExtractor.ExtractTypeDefinition(TypeDefNode, Node: TCodeTreeNode); var Child: TCodeTreeNode; + TypeName: String; begin while Node <> nil do begin @@ -479,7 +736,8 @@ procedure TSymbolExtractor.ExtractTypeDefinition(TypeDefNode, Node: TCodeTreeNod case Node.Desc of ctnClass,ctnClassHelper,ctnRecordHelper,ctnTypeHelper: begin - AddSymbol(TypeDefNode, TSymbolKind._Class); + TypeName := GetIdentifierAtPos(Tool, TypeDefNode.StartPos, true, true); + Builder.AddClass(TypeDefNode, TypeName); end; ctnObject,ctnRecordType: begin @@ -488,7 +746,8 @@ procedure TSymbolExtractor.ExtractTypeDefinition(TypeDefNode, Node: TCodeTreeNod ctnObjCClass,ctnObjCCategory,ctnObjCProtocol: begin // todo: ignore forward defs! - AddSymbol(TypeDefNode, TSymbolKind._Class); + TypeName := GetIdentifierAtPos(Tool, TypeDefNode.StartPos, true, true); + Builder.AddClass(TypeDefNode, TypeName); Inc(IndentLevel); ExtractObjCClassMethods(TypeDefNode, Node.FirstChild); Dec(IndentLevel); @@ -497,7 +756,8 @@ procedure TSymbolExtractor.ExtractTypeDefinition(TypeDefNode, Node: TCodeTreeNod begin // todo: is this a class/record??? PrintNodeDebug(Node.FirstChild, true); - AddSymbol(TypeDefNode, TSymbolKind._Class); + TypeName := GetIdentifierAtPos(Tool, TypeDefNode.StartPos, true, true); + Builder.AddClass(TypeDefNode, TypeName); end; ctnEnumerationType: begin @@ -559,8 +819,13 @@ function TSymbolExtractor.ExtractProcedure(ParentNode, Node: TCodeTreeNode):TSym end; end; - Symbol := AddSymbol(Node, TSymbolKind._Function, Name); - Symbol.containerName:=containerName; + // Create symbol for overload tracking metadata only + // Builder will handle actual addition to Entry.Symbols or FRootSymbols + Symbol := TSymbol.Create(nil); + Symbol.name := Name; + Symbol.kind := TSymbolKind._Function; + Symbol.containerName := containerName; + OverloadMap.Add(Key, Symbol); // recurse into procedures to find nested procedures @@ -668,16 +933,36 @@ procedure TSymbolExtractor.ExtractCodeSection(Node: TCodeTreeNode); Symbol:= ExtractProcedure(nil, Node); - if (Symbol<>nil) and (Symbol.containerName<>'') then + if (Symbol<>nil) then begin - if (LastClassSymbol=nil) or (Symbol.containerName<>LastClassSymbol.name) then - begin - LastClassSymbol:=AddSymbol(Node,TSymbolKind._Class,Symbol.containerName); - end - else - begin - LastClassSymbol.location.range.&end:=Symbol.location.range.&end; - end; + // Use Builder to add methods or global functions based on containerName + if Symbol.containerName<>'' then + begin + // This is a class method + Builder.AddMethod(Node, Symbol.containerName, Symbol.name); + + // In flat mode, we also need to track class symbols for range updates + if Builder.Mode = smFlat then + begin + if (LastClassSymbol=nil) or (Symbol.containerName<>LastClassSymbol.name) then + LastClassSymbol:=AddSymbol(Node,TSymbolKind._Class,Symbol.containerName) + else + LastClassSymbol.location.range.&end:=Symbol.location.range.&end; + end; + end + else + begin + // This is a global function + // In hierarchical mode, skip interface declarations + // to avoid duplicates - only show implementations + if (Builder.Mode = smHierarchical) and + (CodeSection = ctnInterface) then + begin + // Skip interface declaration - will be added from implementation + end + else + Builder.AddGlobalFunction(Node, Symbol.name); + end; end; end; @@ -692,12 +977,15 @@ constructor TSymbolExtractor.Create(_Entry: TSymbolTableEntry; _Code: TCodeBuffe Entry := _Entry; Code := _Code; Tool := _Tool; + Builder := TSymbolBuilder.Create(Entry, Tool, GetSymbolMode); OverloadMap := TFPHashList.Create; RelatedFiles := TFPHashList.Create; end; -destructor TSymbolExtractor.Destroy; +destructor TSymbolExtractor.Destroy; begin + Builder.SerializeSymbols; + Builder.Free; OverloadMap.Free; RelatedFiles.Free; inherited; @@ -1186,10 +1474,12 @@ procedure TSymbolManager.Reload(Code: TCodeBuffer; Always: Boolean = false); try Extractor.ExtractCodeSection(Tool.Tree.Root); finally - Extractor.Free; + Extractor.Free; // This calls Builder.SerializeSymbols in the destructor end; - Entry.SerializeSymbols; + // Note: Entry.fRawJSON is already set by Builder.SerializeSymbols in Extractor.Destroy + // Don't call Entry.SerializeSymbols here as it would overwrite with flat format! + DoLog('Reloaded %s in %d ms', [Code.FileName, MilliSecondsBetween(Now,StartTime)]); end; From 1f9d7fe1fae67066ea5a121ccba4918ab9a4abc1 Mon Sep 17 00:00:00 2001 From: zen010101 <60574100+zen010101@users.noreply.github.com> Date: Fri, 26 Dec 2025 01:38:38 +0800 Subject: [PATCH 2/2] Add Property and Field symbol extraction to DocumentSymbol - Implemented extraction of Property symbols (SymbolKind.Property = 7) - Implemented extraction of Field symbols (SymbolKind.Field = 8) - Both flat and hierarchical modes are supported - Added FPCUnit test suite (Tests.DocumentSymbol) with comprehensive coverage - Tests verify correct extraction of Class, Property, Field, and Method symbols The implementation properly handles both SymbolInformation (flat) and DocumentSymbol (hierarchical) response formats based on client capabilities. --- src/serverprotocol/PasLS.Symbols.pas | 121 +++++++++++++++- src/tests/TestClassWithProperty.pas | 31 +++++ src/tests/Tests.DocumentSymbol.pas | 201 +++++++++++++++++++++++++++ src/tests/testlsp.lpi | 12 ++ src/tests/testlsp.lpr | 2 +- 5 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 src/tests/TestClassWithProperty.pas create mode 100644 src/tests/Tests.DocumentSymbol.pas diff --git a/src/serverprotocol/PasLS.Symbols.pas b/src/serverprotocol/PasLS.Symbols.pas index 7be7f77..ca04fee 100644 --- a/src/serverprotocol/PasLS.Symbols.pas +++ b/src/serverprotocol/PasLS.Symbols.pas @@ -108,6 +108,8 @@ TSymbolBuilder = class function AddClass(Node: TCodeTreeNode; const Name: String): TSymbol; function AddMethod(Node: TCodeTreeNode; const AClassName, AMethodName: String): TSymbol; function AddGlobalFunction(Node: TCodeTreeNode; const Name: String): TSymbol; + function AddProperty(Node: TCodeTreeNode; const AClassName, APropertyName: String): TSymbol; + function AddField(Node: TCodeTreeNode; const AClassName, AFieldName: String): TSymbol; // Serialization procedure SerializeSymbols; @@ -465,6 +467,88 @@ function TSymbolBuilder.AddGlobalFunction(Node: TCodeTreeNode; const Name: Strin end; end; +function TSymbolBuilder.AddProperty(Node: TCodeTreeNode; const AClassName, APropertyName: String): TSymbol; +var + ClassSymbol: TDocumentSymbolEx; + PropertySymbol: TDocumentSymbolEx; + CodePos, EndPos: TCodeXYPosition; +begin + case FMode of + smFlat: + begin + // Flat mode: add property with containerName + if (FTool <> nil) and (Node <> nil) then + begin + FTool.CleanPosToCaret(Node.StartPos, CodePos); + FTool.CleanPosToCaret(Node.EndPos, EndPos); + Result := FEntry.AddSymbol(APropertyName, TSymbolKind._Property, + CodePos.Code.FileName, + CodePos.Y, CodePos.X, + EndPos.Y, EndPos.X); + if Result <> nil then + Result.containerName := AClassName; + end + else + Result := nil; + end; + + smHierarchical: + begin + // Hierarchical mode: add property to class's children + ClassSymbol := FindOrCreateClass(AClassName, Node); + if ClassSymbol <> nil then + begin + PropertySymbol := TDocumentSymbolEx.Create(ClassSymbol.children); + PropertySymbol.name := APropertyName; + PropertySymbol.kind := TSymbolKind._Property; + SetNodeRange(PropertySymbol, Node); + end; + Result := nil; // Hierarchical symbols are not TSymbol + end; + end; +end; + +function TSymbolBuilder.AddField(Node: TCodeTreeNode; const AClassName, AFieldName: String): TSymbol; +var + ClassSymbol: TDocumentSymbolEx; + FieldSymbol: TDocumentSymbolEx; + CodePos, EndPos: TCodeXYPosition; +begin + case FMode of + smFlat: + begin + // Flat mode: add field with containerName + if (FTool <> nil) and (Node <> nil) then + begin + FTool.CleanPosToCaret(Node.StartPos, CodePos); + FTool.CleanPosToCaret(Node.EndPos, EndPos); + Result := FEntry.AddSymbol(AFieldName, TSymbolKind._Field, + CodePos.Code.FileName, + CodePos.Y, CodePos.X, + EndPos.Y, EndPos.X); + if Result <> nil then + Result.containerName := AClassName; + end + else + Result := nil; + end; + + smHierarchical: + begin + // Hierarchical mode: add field to class's children + ClassSymbol := FindOrCreateClass(AClassName, Node); + if ClassSymbol <> nil then + begin + FieldSymbol := TDocumentSymbolEx.Create(ClassSymbol.children); + FieldSymbol.name := AFieldName; + FieldSymbol.kind := TSymbolKind._Field; + SetNodeRange(FieldSymbol, Node); + end; + Result := nil; // Hierarchical symbols are not TSymbol + end; + end; +end; + procedure TSymbolBuilder.SerializeSymbols; var SerializedItems: TJSONArray; @@ -677,7 +761,8 @@ procedure TSymbolExtractor.ExtractObjCClassMethods(ClassNode, Node: TCodeTreeNod var Child: TCodeTreeNode; ExternalClass: boolean = false; - TypeName: String; + TypeName, PropertyName, FieldName: String; + i: Integer; begin while Node <> nil do begin @@ -702,6 +787,30 @@ procedure TSymbolExtractor.ExtractObjCClassMethods(ClassNode, Node: TCodeTreeNod begin AddSymbol(Node, TSymbolKind._Method, Tool.ExtractProcName(Node, [])); end; + ctnProperty: + begin + // For property, skip the "property" keyword to get the actual property name + Tool.MoveCursorToCleanPos(Node.StartPos); + Tool.ReadNextAtom; // Skip "property" keyword + Tool.ReadNextAtom; // Move to property name + TypeName := GetIdentifierAtPos(Tool, ClassNode.StartPos, true, true); + // Extract property name from current atom + PropertyName := Copy(Tool.Scanner.CleanedSrc, Tool.CurPos.StartPos, + Tool.CurPos.EndPos - Tool.CurPos.StartPos); + Builder.AddProperty(Node, TypeName, PropertyName); + end; + ctnVarDefinition: + begin + // Extract field (class member variable) + TypeName := GetIdentifierAtPos(Tool, ClassNode.StartPos, true, true); + // For field, extract identifier without the colon + FieldName := GetIdentifierAtPos(Tool, Node.StartPos, true, true); + // Remove trailing colon if present + i := Pos(':', FieldName); + if i > 0 then + FieldName := Copy(FieldName, 1, i - 1); + Builder.AddField(Node, TypeName, FieldName); + end; ctnClassPublic,ctnClassPublished,ctnClassPrivate,ctnClassProtected, ctnClassRequired,ctnClassOptional: if ExternalClass then @@ -717,6 +826,13 @@ procedure TSymbolExtractor.ExtractObjCClassMethods(ClassNode, Node: TCodeTreeNod AddSymbol(Node, TSymbolKind._Method, TypeName+'.'+Tool.ExtractProcName(Child, [])); Child := Child.NextBrother; end; + end + else + begin + // For regular Pascal classes, recurse into visibility sections + Inc(IndentLevel); + ExtractObjCClassMethods(ClassNode, Node.FirstChild); + Dec(IndentLevel); end; end; @@ -738,6 +854,9 @@ procedure TSymbolExtractor.ExtractTypeDefinition(TypeDefNode, Node: TCodeTreeNod begin TypeName := GetIdentifierAtPos(Tool, TypeDefNode.StartPos, true, true); Builder.AddClass(TypeDefNode, TypeName); + Inc(IndentLevel); + ExtractObjCClassMethods(TypeDefNode, Node.FirstChild); + Dec(IndentLevel); end; ctnObject,ctnRecordType: begin diff --git a/src/tests/TestClassWithProperty.pas b/src/tests/TestClassWithProperty.pas new file mode 100644 index 0000000..ab1a8e1 --- /dev/null +++ b/src/tests/TestClassWithProperty.pas @@ -0,0 +1,31 @@ +unit TestClassWithProperty; + +{$mode objfpc}{$H+} + +interface + +type + TUser = class + private + FName: String; + FAge: Integer; + public + property Name: String read FName write FName; + property Age: Integer read FAge write FAge; + procedure PrintInfo; + function GetFullName: String; + end; + +implementation + +procedure TUser.PrintInfo; +begin + writeln('User: ', FName, ', Age: ', FAge); +end; + +function TUser.GetFullName: String; +begin + Result := FName; +end; + +end. diff --git a/src/tests/Tests.DocumentSymbol.pas b/src/tests/Tests.DocumentSymbol.pas new file mode 100644 index 0000000..f3c7b91 --- /dev/null +++ b/src/tests/Tests.DocumentSymbol.pas @@ -0,0 +1,201 @@ +unit Tests.DocumentSymbol; + +{$mode objfpc}{$H+} + +interface + +uses + Classes, SysUtils, fpcunit, testregistry, + CodeToolManager, CodeCache, + PasLS.Symbols; + +type + + { TTestDocumentSymbol } + + TTestDocumentSymbol = class(TTestCase) + private + FTestCode: TCodeBuffer; + FTestFile: String; + procedure CreateTestFile(const AContent: String); + procedure CleanupTestFile; + protected + procedure SetUp; override; + procedure TearDown; override; + published + procedure TestSymbolExtractionHierarchical; + procedure TestSymbolExtractionFlat; + end; + +implementation + +const + TEST_UNIT_WITH_PROPERTY_AND_FIELD = + 'unit TestUnit;' + LineEnding + + '' + LineEnding + + '{$mode objfpc}{$H+}' + LineEnding + + '' + LineEnding + + 'interface' + LineEnding + + '' + LineEnding + + 'type' + LineEnding + + ' TUser = class' + LineEnding + + ' private' + LineEnding + + ' FName: String;' + LineEnding + + ' FAge: Integer;' + LineEnding + + ' public' + LineEnding + + ' property Name: String read FName write FName;' + LineEnding + + ' property Age: Integer read FAge write FAge;' + LineEnding + + ' procedure PrintInfo;' + LineEnding + + ' function GetFullName: String;' + LineEnding + + ' end;' + LineEnding + + '' + LineEnding + + 'implementation' + LineEnding + + '' + LineEnding + + 'procedure TUser.PrintInfo;' + LineEnding + + 'begin' + LineEnding + + ' writeln(FName);' + LineEnding + + 'end;' + LineEnding + + '' + LineEnding + + 'function TUser.GetFullName: String;' + LineEnding + + 'begin' + LineEnding + + ' Result := FName;' + LineEnding + + 'end;' + LineEnding + + '' + LineEnding + + 'end.'; + +{ TTestDocumentSymbol } + +procedure TTestDocumentSymbol.CreateTestFile(const AContent: String); +var + F: TextFile; +begin + FTestFile := GetTempFileName('', 'testunit'); + FTestFile := ChangeFileExt(FTestFile, '.pas'); + + AssignFile(F, FTestFile); + try + Rewrite(F); + Write(F, AContent); + finally + CloseFile(F); + end; +end; + +procedure TTestDocumentSymbol.CleanupTestFile; +begin + if FileExists(FTestFile) then + DeleteFile(FTestFile); + FTestFile := ''; +end; + +procedure TTestDocumentSymbol.SetUp; +begin + inherited SetUp; + FTestCode := nil; + FTestFile := ''; + + // Ensure SymbolManager is initialized + if SymbolManager = nil then + SymbolManager := TSymbolManager.Create; + + // Set hierarchical mode for tests + SetClientCapabilities(True); +end; + +procedure TTestDocumentSymbol.TearDown; +begin + CleanupTestFile; + FTestCode := nil; + inherited TearDown; +end; + +procedure TTestDocumentSymbol.TestSymbolExtractionHierarchical; +var + RawJSON: String; +begin + // Ensure hierarchical mode + SetClientCapabilities(True); + + // Create test file + CreateTestFile(TEST_UNIT_WITH_PROPERTY_AND_FIELD); + + // Load code buffer + FTestCode := CodeToolBoss.LoadFile(FTestFile, True, False); + AssertNotNull('Code buffer should be loaded', FTestCode); + + // Use SymbolManager to reload and extract symbols (public API) + SymbolManager.Reload(FTestCode, True); + + // Get the raw JSON from SymbolManager + RawJSON := SymbolManager.FindDocumentSymbols(FTestFile).AsJSON; + + // Verify we extracted symbols + AssertTrue('Should have extracted symbols', RawJSON <> ''); + + // Verify the extracted symbols contain our expected names + AssertTrue('JSON should contain TUser class', Pos('"TUser"', RawJSON) > 0); + AssertTrue('JSON should contain FName field', Pos('FName', RawJSON) > 0); + AssertTrue('JSON should contain FAge field', Pos('FAge', RawJSON) > 0); + AssertTrue('JSON should contain Name property', Pos('"Name"', RawJSON) > 0); + AssertTrue('JSON should contain Age property', Pos('"Age"', RawJSON) > 0); + AssertTrue('JSON should contain PrintInfo method', Pos('PrintInfo', RawJSON) > 0); + AssertTrue('JSON should contain GetFullName method', Pos('GetFullName', RawJSON) > 0); + + // Check for hierarchical structure (children array) + AssertTrue('Should have children in hierarchical mode', Pos('"children"', RawJSON) > 0); + + // Check for correct symbol kinds (note: JSON has spaces around colons) + AssertTrue('Should have Class kind (5)', Pos('"kind" : 5', RawJSON) > 0); + AssertTrue('Should have Field kind (8)', Pos('"kind" : 8', RawJSON) > 0); + AssertTrue('Should have Property kind (7)', Pos('"kind" : 7', RawJSON) > 0); + AssertTrue('Should have Function/Method kind (12)', Pos('"kind" : 12', RawJSON) > 0); +end; + +procedure TTestDocumentSymbol.TestSymbolExtractionFlat; +var + RawJSON: String; +begin + // Ensure flat mode + SetClientCapabilities(False); + + // Create test file + CreateTestFile(TEST_UNIT_WITH_PROPERTY_AND_FIELD); + + // Load code buffer + FTestCode := CodeToolBoss.LoadFile(FTestFile, True, False); + AssertNotNull('Code buffer should be loaded', FTestCode); + + // Use SymbolManager to reload and extract symbols (public API) + SymbolManager.Reload(FTestCode, True); + + // Get the raw JSON from SymbolManager + RawJSON := SymbolManager.FindDocumentSymbols(FTestFile).AsJSON; + + // Verify we extracted symbols + AssertTrue('Should have extracted symbols', RawJSON <> ''); + + // Verify the extracted symbols contain our expected names + AssertTrue('JSON should contain TUser class', Pos('"TUser"', RawJSON) > 0); + AssertTrue('JSON should contain FName field', Pos('FName', RawJSON) > 0); + AssertTrue('JSON should contain FAge field', Pos('FAge', RawJSON) > 0); + AssertTrue('JSON should contain Name property', Pos('"Name"', RawJSON) > 0); + AssertTrue('JSON should contain Age property', Pos('"Age"', RawJSON) > 0); + AssertTrue('JSON should contain PrintInfo method', Pos('PrintInfo', RawJSON) > 0); + AssertTrue('JSON should contain GetFullName method', Pos('GetFullName', RawJSON) > 0); + + // In flat mode, should NOT have children array + AssertTrue('Should NOT have children in flat mode', Pos('"children"', RawJSON) = 0); + + // In flat mode, should have containerName for properties and fields + AssertTrue('Should have containerName in flat mode', Pos('"containerName"', RawJSON) > 0); + + // Check for correct symbol kinds (note: JSON has spaces around colons) + AssertTrue('Should have Class kind (5)', Pos('"kind" : 5', RawJSON) > 0); + AssertTrue('Should have Field kind (8)', Pos('"kind" : 8', RawJSON) > 0); + AssertTrue('Should have Property kind (7)', Pos('"kind" : 7', RawJSON) > 0); + AssertTrue('Should have Function/Method kind (12)', Pos('"kind" : 12', RawJSON) > 0); +end; + +initialization + RegisterTest(TTestDocumentSymbol); +end. diff --git a/src/tests/testlsp.lpi b/src/tests/testlsp.lpi index f669640..7179345 100644 --- a/src/tests/testlsp.lpi +++ b/src/tests/testlsp.lpi @@ -38,6 +38,18 @@ + + + + + + + + + + + + diff --git a/src/tests/testlsp.lpr b/src/tests/testlsp.lpr index 7d2d157..36a8ea3 100644 --- a/src/tests/testlsp.lpr +++ b/src/tests/testlsp.lpr @@ -3,7 +3,7 @@ {$mode objfpc}{$H+} uses - Classes, consoletestrunner, Tests.Basic; + Classes, consoletestrunner, Tests.Basic, Tests.DocumentSymbol; type