From d80cd808e63066bdd0942dfd2ef9bc9887d1cb3c Mon Sep 17 00:00:00 2001 From: Rio Sanjaya Date: Tue, 1 Sep 2026 15:33:06 +0700 Subject: [PATCH 1/5] first investigate goto definition on comments --- src/serverprotocol/PasLS.GotoDefinition.pas | 41 +++++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/src/serverprotocol/PasLS.GotoDefinition.pas b/src/serverprotocol/PasLS.GotoDefinition.pas index 7e6aaf9..832a6d0 100644 --- a/src/serverprotocol/PasLS.GotoDefinition.pas +++ b/src/serverprotocol/PasLS.GotoDefinition.pas @@ -27,9 +27,9 @@ interface { RTL } Classes, { Code Tools } - CodeToolManager, CodeCache, + CodeToolManager, CodeCache, BasicCodeTools, { Protocol } - LSP.Base, LSP.Basic; + LSP.Basetak , LSP.Basic; type @@ -48,8 +48,37 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca var Code: TCodeBuffer; NewCode: TCodeBuffer; - X, Y: Integer; + X, Y, AbsPos: Integer; NewX, NewY, NewTopLine: integer; + + function CheckPlainComments( + Source: string; + CurAbsPos: integer + ): boolean; + var + Filename: String; + p, EndPos : Integer; + begin + // check if cursor in a comment (ignoring directives) + Result := false; + if (CurAbsPos < 1) or (CurAbsPos > length(Source)) then exit; + p := 1; + repeat + p := FindNextComment(Source, p); + if p > CurAbsPos then break; + EndPos := + FindCommentEnd( + Source, + p, + CodeToolBoss.CurCodeTool.Scanner.NestedComments + ); + DoLog('Endpos %d; CurabsPos %d; snip: %s', [EndPos, CurAbsPos, copy(Source, CurAbsPos - 50, 100)]); + if EndPos > CurAbsPos then + exit(true); + p := EndPos; + until false; + end; + begin with Params do begin Code := CodeToolBoss.FindFile(textDocument.localPath); @@ -79,7 +108,11 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca else begin Result := nil; - PublishCodeToolsError(Transport,''); + Code.LineColToPosition(x, y, AbsPos); + if not CheckPlainComments(Code.Source, AbsPos) then + begin + PublishCodeToolsError(Transport,''); + end; end; end; end; From 9223b4cbe54dd0a3cbc3a9a8f8c59ed5e13cfe24 Mon Sep 17 00:00:00 2001 From: Rio Sanjaya Date: Wed, 2 Sep 2026 00:49:28 +0700 Subject: [PATCH 2/5] Improve GotoDefinition context detection Replace the manual comment-checking function with a more robust GetContextAtPosition procedure that tracks strings and different types of comments during parsing. This prevents trigger definition lookups while the cursor is inside strings or comment blocks. --- src/serverprotocol/PasLS.GotoDefinition.pas | 138 ++++++++++++++------ 1 file changed, 97 insertions(+), 41 deletions(-) diff --git a/src/serverprotocol/PasLS.GotoDefinition.pas b/src/serverprotocol/PasLS.GotoDefinition.pas index 832a6d0..81f3e21 100644 --- a/src/serverprotocol/PasLS.GotoDefinition.pas +++ b/src/serverprotocol/PasLS.GotoDefinition.pas @@ -25,11 +25,11 @@ interface uses { RTL } - Classes, + Classes, sysutils, { Code Tools } - CodeToolManager, CodeCache, BasicCodeTools, + CodeToolManager, CodeCache, BasicCodeTools, CodeTree, { Protocol } - LSP.Basetak , LSP.Basic; + LSP.Base , LSP.Basic; type @@ -51,32 +51,88 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca X, Y, AbsPos: Integer; NewX, NewY, NewTopLine: integer; - function CheckPlainComments( - Source: string; - CurAbsPos: integer - ): boolean; + IsString, IsComment: Boolean; + + procedure GetContextAtPosition(CodeBuffer: TCodeBuffer; CaretX, CaretY: Integer; + out IsString, IsComment: Boolean); var - Filename: String; - p, EndPos : Integer; + Tool: TCodeTool; + CleanPos, RealPos, i: Integer; + InStr, InLineComment, InBlock1, InBlock2: Boolean; + Src: String; begin - // check if cursor in a comment (ignoring directives) - Result := false; - if (CurAbsPos < 1) or (CurAbsPos > length(Source)) then exit; - p := 1; - repeat - p := FindNextComment(Source, p); - if p > CurAbsPos then break; - EndPos := - FindCommentEnd( - Source, - p, - CodeToolBoss.CurCodeTool.Scanner.NestedComments - ); - DoLog('Endpos %d; CurabsPos %d; snip: %s', [EndPos, CurAbsPos, copy(Source, CurAbsPos - 50, 100)]); - if EndPos > CurAbsPos then - exit(true); - p := EndPos; - until false; + IsString := False; + IsComment := False; + + if not CodeToolBoss.Explore(CodeBuffer, Tool, False) or (Tool = nil) then + Exit; + + CodeBuffer.LineColToPosition(CaretY, CaretX, RealPos); + + if RealPos < 1 then + Exit; + + Src := CodeBuffer.Source; + InStr := False; + InLineComment := False; // // ... + InBlock1 := False; // { ... } + InBlock2 := False; // (* ... *) + + i := 0; + while i < RealPos do + begin + // Line comments terminate at line breaks + if Src[i] in [#13, #10] then + InLineComment := False; + + if not InBlock1 and not InBlock2 and not InLineComment then + begin + if Src[i] = '''' then + begin + InStr := not InStr + end + else + begin + if not InStr then + begin + if Src[i] = '{' then + InBlock1 := True + else if (i < Length(Src)) and (Src[i] = '(') and (Src[i+1] = '*') then + begin + InBlock2 := True; + Inc(i); // Skip the '*' + end + else if (i < Length(Src)) and (Src[i] = '/') and (Src[i+1] = '/') then + begin + InLineComment := True; + Inc(i); // Skip the second '/' + end; + end; + end; + end + else + begin + // Trick: If we've reached the caret position exactly, break out early. + // This ensures that clicking exactly on a closing '}' still registers + // as being "inside" the comment block. + if i = RealPos then Break; + + // Check for block comment terminators + if InBlock1 and (Src[i] = '}') then + InBlock1 := False + else if InBlock2 and (i < Length(Src)) and (Src[i] = '*') and (Src[i+1] = ')') then + begin + InBlock2 := False; + Inc(i); // Skip the ')' + end; + end; + + Inc(i); + end; + + // Final evaluation + IsComment := InBlock1 or InBlock2 or InLineComment; + IsString := InStr; end; begin with Params do @@ -84,6 +140,9 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca Code := CodeToolBoss.FindFile(textDocument.localPath); X := position.character; Y := position.line; + + GetContextAtPosition(Code, X + 1, Y + 1, IsString, IsComment); + { NOTE: Use FindMainDeclaration to skip forward declarations and find the main/complete declaration. This is the correct behavior for @@ -99,21 +158,18 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca FindMainDeclaration returns the main declaration location. } - if CodeToolBoss.FindMainDeclaration(Code, X + 1, Y + 1, NewCode, NewX, NewY, NewTopLine) then - begin - Result := TLocation.Create; - Result.uri := PathToURI(NewCode.Filename); - Result.range := GetIdentifierRangeAtPos(NewCode, NewX, NewY - 1); - end - else - begin - Result := nil; - Code.LineColToPosition(x, y, AbsPos); - if not CheckPlainComments(Code.Source, AbsPos) then - begin + if not IsString and not IsComment then + if CodeToolBoss.FindMainDeclaration(Code, X + 1, Y + 1, NewCode, NewX, NewY, NewTopLine) then + begin + Result := TLocation.Create; + Result.uri := PathToURI(NewCode.Filename); + Result.range := GetIdentifierRangeAtPos(NewCode, NewX, NewY - 1); + end + else + begin + Result := nil; PublishCodeToolsError(Transport,''); - end; - end; + end; end; end; From 37536956d324698a02d8e0c03b79b15800d1a254 Mon Sep 17 00:00:00 2001 From: Rio Sanjaya Date: Thu, 3 Sep 2026 10:51:06 +0700 Subject: [PATCH 3/5] Ignore goto definition requests on Pascal keywords --- src/serverprotocol/PasLS.GotoDefinition.pas | 58 +++++++++++++++++---- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/src/serverprotocol/PasLS.GotoDefinition.pas b/src/serverprotocol/PasLS.GotoDefinition.pas index 81f3e21..60e7d96 100644 --- a/src/serverprotocol/PasLS.GotoDefinition.pas +++ b/src/serverprotocol/PasLS.GotoDefinition.pas @@ -51,21 +51,32 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca X, Y, AbsPos: Integer; NewX, NewY, NewTopLine: integer; - IsString, IsComment: Boolean; + IsString, IsComment, isKeyword: Boolean; procedure GetContextAtPosition(CodeBuffer: TCodeBuffer; CaretX, CaretY: Integer; - out IsString, IsComment: Boolean); + out IsString, IsComment, isKeyword: Boolean); + const + // A comprehensive list of Free Pascal / Delphi reserved keywords + PascalKeywords: array[0..67] of string = ( + 'absolute', 'and', 'array', 'as', 'asm', 'begin', 'case', 'class', 'const', + 'constructor', 'destructor', 'dispose', 'div', 'do', 'downto', 'else', + 'end', 'except', 'exports', 'file', 'finalization', 'finally', 'for', + 'function', 'goto', 'if', 'implementation', 'in', 'inherited', + 'initialization', 'inline', 'interface', 'is', 'label', 'library', 'mod', + 'new', 'nil', 'not', 'object', 'of', 'operator', 'or', 'out', 'packed', + 'procedure', 'program', 'property', 'raise', 'record', 'repeat', + 'resourcestring', 'set', 'shl', 'shr', 'string', 'then', 'threadvar', + 'to', 'try', 'type', 'unit', 'until', 'uses', 'var', 'while', 'with', 'xor' + ); var - Tool: TCodeTool; - CleanPos, RealPos, i: Integer; + RealPos, i: Integer; + StartWord, EndWord: Integer; InStr, InLineComment, InBlock1, InBlock2: Boolean; - Src: String; + Src, CurrentWord, TestWord: String; begin IsString := False; IsComment := False; - - if not CodeToolBoss.Explore(CodeBuffer, Tool, False) or (Tool = nil) then - Exit; + isKeyword := False; CodeBuffer.LineColToPosition(CaretY, CaretX, RealPos); @@ -78,8 +89,8 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca InBlock1 := False; // { ... } InBlock2 := False; // (* ... *) - i := 0; - while i < RealPos do + i := 1; + while i <= RealPos do begin // Line comments terminate at line breaks if Src[i] in [#13, #10] then @@ -133,6 +144,31 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca // Final evaluation IsComment := InBlock1 or InBlock2 or InLineComment; IsString := InStr; + + if not (IsString or IsComment) then + begin + if Src[RealPos] in ['a'..'z', 'A'..'Z', '0'..'9', '_'] then + begin + StartWord := RealPos; + while (StartWord > 1) and (Src[StartWord - 1] in ['a'..'z', 'A'..'Z', '0'..'9', '_']) do + Dec(StartWord); + + EndWord := RealPos; + while (EndWord < Length(Src)) and (Src[EndWord + 1] in ['a'..'z', 'A'..'Z', '0'..'9', '_']) do + Inc(EndWord); + + CurrentWord := Copy(Src, StartWord, EndWord - StartWord + 1); + end; + + for TestWord in PascalKeywords do + begin + if CompareTextCT(TestWord, CurrentWord) = 0 then + begin + isKeyword := True; + break; + end; + end; + end; end; begin with Params do @@ -141,7 +177,7 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca X := position.character; Y := position.line; - GetContextAtPosition(Code, X + 1, Y + 1, IsString, IsComment); + GetContextAtPosition(Code, X + 1, Y + 1, IsString, IsComment, isKeyword); { NOTE: Use FindMainDeclaration to skip forward declarations and find From 6411e26968c972f2f73ef99f02d4b3fafdab19c4 Mon Sep 17 00:00:00 2001 From: Rio Sanjaya Date: Thu, 3 Sep 2026 20:01:47 +0700 Subject: [PATCH 4/5] Expand Pascal keyword list and fix Access Violation --- src/serverprotocol/PasLS.GotoDefinition.pas | 51 ++++++++++++--------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/serverprotocol/PasLS.GotoDefinition.pas b/src/serverprotocol/PasLS.GotoDefinition.pas index 60e7d96..304b7b8 100644 --- a/src/serverprotocol/PasLS.GotoDefinition.pas +++ b/src/serverprotocol/PasLS.GotoDefinition.pas @@ -57,16 +57,18 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca out IsString, IsComment, isKeyword: Boolean); const // A comprehensive list of Free Pascal / Delphi reserved keywords - PascalKeywords: array[0..67] of string = ( - 'absolute', 'and', 'array', 'as', 'asm', 'begin', 'case', 'class', 'const', - 'constructor', 'destructor', 'dispose', 'div', 'do', 'downto', 'else', - 'end', 'except', 'exports', 'file', 'finalization', 'finally', 'for', - 'function', 'goto', 'if', 'implementation', 'in', 'inherited', - 'initialization', 'inline', 'interface', 'is', 'label', 'library', 'mod', - 'new', 'nil', 'not', 'object', 'of', 'operator', 'or', 'out', 'packed', - 'procedure', 'program', 'property', 'raise', 'record', 'repeat', - 'resourcestring', 'set', 'shl', 'shr', 'string', 'then', 'threadvar', - 'to', 'try', 'type', 'unit', 'until', 'uses', 'var', 'while', 'with', 'xor' + PascalKeywords: array[0..104] of string = ( + 'absolute','abstract','alias','and','array','as','asm','assembler','begin','break','case', + 'cdecl','class','const','constref','constructor','continue','Cppdecl','default','destructor', + 'dispose','div','do','downto','else','end','except','exit','export','exports','external', + 'false','file','finalization','finally','for','forward','function','generic','goto','if', + 'implementation','in','index','inherited','initialization','inline','interface','is','label', + 'library','local','mod','name','new','nil','nostackframe','not','object','of','oldfpccall', + 'on','operator','or','out','override','packed','pascal','private','procedure','program', + 'property','protected','public','published','raise','read','record','register','reintroduce', + 'repeat','safecall','self','set','shl','shr','softfloat','specialize','stdcall','string', + 'then','threadvar','to','true','try','type','unit','until','uses','var','virtual','while', + 'with','write','xor' ); var RealPos, i: Integer; @@ -194,18 +196,25 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca FindMainDeclaration returns the main declaration location. } - if not IsString and not IsComment then - if CodeToolBoss.FindMainDeclaration(Code, X + 1, Y + 1, NewCode, NewX, NewY, NewTopLine) then - begin - Result := TLocation.Create; - Result.uri := PathToURI(NewCode.Filename); - Result.range := GetIdentifierRangeAtPos(NewCode, NewX, NewY - 1); - end - else - begin - Result := nil; + if not (IsString or IsComment or isKeyword) then + begin + if CodeToolBoss.FindMainDeclaration(Code, X + 1, Y + 1, NewCode, NewX, NewY, NewTopLine) then + begin + Result := TLocation.Create; + Result.uri := PathToURI(NewCode.Filename); + Result.range := GetIdentifierRangeAtPos(NewCode, NewX, NewY - 1); + end + else + begin + Result := nil; PublishCodeToolsError(Transport,''); - end; + end; + end + else + begin + Result := nil; + PublishCodeToolsError(Transport,''); + end; end; end; From c2fcbdd7b7cc8cbdbcf2b02b651ad46d1fb64041 Mon Sep 17 00:00:00 2001 From: Rio Sanjaya Date: Fri, 4 Sep 2026 16:14:12 +0700 Subject: [PATCH 5/5] Refactor context checking to use CodeTools API --- src/serverprotocol/PasLS.GotoDefinition.pas | 140 ++++---------------- 1 file changed, 29 insertions(+), 111 deletions(-) diff --git a/src/serverprotocol/PasLS.GotoDefinition.pas b/src/serverprotocol/PasLS.GotoDefinition.pas index 304b7b8..4bf423f 100644 --- a/src/serverprotocol/PasLS.GotoDefinition.pas +++ b/src/serverprotocol/PasLS.GotoDefinition.pas @@ -27,7 +27,7 @@ interface { RTL } Classes, sysutils, { Code Tools } - CodeToolManager, CodeCache, BasicCodeTools, CodeTree, + CodeToolManager, CodeCache, BasicCodeTools, CodeTree, CodeAtom, { Protocol } LSP.Base , LSP.Basic; @@ -53,124 +53,44 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca IsString, IsComment, isKeyword: Boolean; - procedure GetContextAtPosition(CodeBuffer: TCodeBuffer; CaretX, CaretY: Integer; - out IsString, IsComment, isKeyword: Boolean); - const - // A comprehensive list of Free Pascal / Delphi reserved keywords - PascalKeywords: array[0..104] of string = ( - 'absolute','abstract','alias','and','array','as','asm','assembler','begin','break','case', - 'cdecl','class','const','constref','constructor','continue','Cppdecl','default','destructor', - 'dispose','div','do','downto','else','end','except','exit','export','exports','external', - 'false','file','finalization','finally','for','forward','function','generic','goto','if', - 'implementation','in','index','inherited','initialization','inline','interface','is','label', - 'library','local','mod','name','new','nil','nostackframe','not','object','of','oldfpccall', - 'on','operator','or','out','override','packed','pascal','private','procedure','program', - 'property','protected','public','published','raise','read','record','register','reintroduce', - 'repeat','safecall','self','set','shl','shr','softfloat','specialize','stdcall','string', - 'then','threadvar','to','true','try','type','unit','until','uses','var','virtual','while', - 'with','write','xor' - ); + function IsIdentifier(CodeBuffer: TCodeBuffer; CaretX, CaretY: Integer): Boolean; var - RealPos, i: Integer; - StartWord, EndWord: Integer; - InStr, InLineComment, InBlock1, InBlock2: Boolean; - Src, CurrentWord, TestWord: String; + IsString, IsComment, isKeyword: Boolean; + CursorPos: TCodeXYPosition; + CodeTool: TCodeTool; + SameArea: TAtomPosition; + CleanPos: integer; begin IsString := False; IsComment := False; isKeyword := False; - CodeBuffer.LineColToPosition(CaretY, CaretX, RealPos); - - if RealPos < 1 then - Exit; - - Src := CodeBuffer.Source; - InStr := False; - InLineComment := False; // // ... - InBlock1 := False; // { ... } - InBlock2 := False; // (* ... *) + CursorPos.Code := CodeBuffer; + CursorPos.X := CaretX; + CursorPos.Y := CaretY; + CodeTool:=CodeToolBoss.FindCodeToolForSource(CodeBuffer); + + if CodeTool.CaretToCleanPos(CursorPos, CleanPos) <> 0 then + exit; + + CodeTool.BuildTreeAndGetCleanPos(CursorPos, CleanPos); + CodeTool.GetCleanPosInfo(-1, CleanPos, false, SameArea); - i := 1; - while i <= RealPos do - begin - // Line comments terminate at line breaks - if Src[i] in [#13, #10] then - InLineComment := False; - - if not InBlock1 and not InBlock2 and not InLineComment then - begin - if Src[i] = '''' then - begin - InStr := not InStr - end - else - begin - if not InStr then - begin - if Src[i] = '{' then - InBlock1 := True - else if (i < Length(Src)) and (Src[i] = '(') and (Src[i+1] = '*') then - begin - InBlock2 := True; - Inc(i); // Skip the '*' - end - else if (i < Length(Src)) and (Src[i] = '/') and (Src[i+1] = '/') then - begin - InLineComment := True; - Inc(i); // Skip the second '/' - end; - end; - end; - end - else - begin - // Trick: If we've reached the caret position exactly, break out early. - // This ensures that clicking exactly on a closing '}' still registers - // as being "inside" the comment block. - if i = RealPos then Break; - - // Check for block comment terminators - if InBlock1 and (Src[i] = '}') then - InBlock1 := False - else if InBlock2 and (i < Length(Src)) and (Src[i] = '*') and (Src[i+1] = ')') then - begin - InBlock2 := False; - Inc(i); // Skip the ')' - end; - end; - - Inc(i); - end; - - // Final evaluation - IsComment := InBlock1 or InBlock2 or InLineComment; - IsString := InStr; + if SameArea.Flag = cafNone then + IsComment := (SameArea.StartPos <= CleanPos) and (CleanPos < SameArea.EndPos); - if not (IsString or IsComment) then + if not IsComment then begin - if Src[RealPos] in ['a'..'z', 'A'..'Z', '0'..'9', '_'] then - begin - StartWord := RealPos; - while (StartWord > 1) and (Src[StartWord - 1] in ['a'..'z', 'A'..'Z', '0'..'9', '_']) do - Dec(StartWord); - - EndWord := RealPos; - while (EndWord < Length(Src)) and (Src[EndWord + 1] in ['a'..'z', 'A'..'Z', '0'..'9', '_']) do - Inc(EndWord); - - CurrentWord := Copy(Src, StartWord, EndWord - StartWord + 1); - end; + CodeTool.MoveCursorToCleanPos(SameArea.StartPos); + CodeTool.ReadNextAtom; - for TestWord in PascalKeywords do - begin - if CompareTextCT(TestWord, CurrentWord) = 0 then - begin - isKeyword := True; - break; - end; - end; + if CodeTool.AtomIsStringConstant then + IsString := True + else if CodeTool.StringIsKeyWord(CodeTool.GetAtom) then + isKeyword := True; end; + + Result := not (IsString or isKeyword or IsComment); end; begin with Params do @@ -179,8 +99,6 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca X := position.character; Y := position.line; - GetContextAtPosition(Code, X + 1, Y + 1, IsString, IsComment, isKeyword); - { NOTE: Use FindMainDeclaration to skip forward declarations and find the main/complete declaration. This is the correct behavior for @@ -196,7 +114,7 @@ function TGotoDefinition.Process(var Params: TTextDocumentPositionParams): TLoca FindMainDeclaration returns the main declaration location. } - if not (IsString or IsComment or isKeyword) then + if IsIdentifier(Code, X + 1, Y + 1) then begin if CodeToolBoss.FindMainDeclaration(Code, X + 1, Y + 1, NewCode, NewX, NewY, NewTopLine) then begin