Skip to content

Commit 6b02477

Browse files
committed
feat(lsp): navigate local and pattern-bound symbols to declarations
1 parent 9816726 commit 6b02477

7 files changed

Lines changed: 490 additions & 113 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ All notable changes to FScript are documented in this file.
44

55
## [Unreleased]
66

7+
- Added LSP go-to-definition support for local and pattern-bound variable bindings so usages navigate to the nearest in-scope declaration (including function parameters and tuple `let` destructuring names).
8+
79
## [0.41.0]
810

911
- Added `Int/Float/Bool` conversion helpers in stdlib-style builtins (`*.tryParse` and `*.toString`) for safe scalar parsing and string formatting.

docs/specs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ Normative behavior for the language, runtime surface, hosting model, and editor/
2424
- LSP inlay hints: [`lsp-inlay-hints.md`](./lsp-inlay-hints.md)
2525
- LSP uses runtime extern schemes for typing/signatures and resolves navigation to included-file declarations.
2626
- LSP injected stdlib functions show named-argument signatures when available and resolve definition to readonly virtual stdlib sources (`fscript-stdlib:///...`).
27+
- LSP go-to-definition resolves local variable usages to their nearest in-scope local declaration (including function parameters and local `let` bindings).
2728
- Definition/type-definition from record field labels in function return record literals resolves to the declared return type (including import-provided types).

docs/specs/lsp-inlay-hints.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ Behavior:
7676
- `fscript-stdlib:///List.fss`
7777
- `fscript-stdlib:///Map.fss`
7878

79+
## Local Binding Definition Navigation
80+
81+
- Go-to-definition on a local variable usage resolves to the nearest lexical local declaration in scope.
82+
- This includes function parameters, lambda parameters, local `let` bindings, and pattern-bound variables.
83+
7984
## Related specifications
8085

8186
- Syntax and indentation: [`syntax-and-indentation.md`](./syntax-and-indentation.md)

src/FScript.CSharpInterop/LanguageServer/LspHandlers.fs

Lines changed: 68 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -690,78 +690,88 @@ module LspHandlers =
690690
| Some includeLoc ->
691691
LspProtocol.sendResponse idNode (Some includeLoc)
692692
| None ->
693-
let localSymbol = tryResolveSymbol doc line character
694-
let wordAtCursor = tryGetWordAtPosition doc.Text line character
695-
696-
let symbolAndUri =
697-
match localSymbol with
698-
| Some sym ->
699-
match tryUriFromSpanFile uri sym.Span with
700-
| Some targetUri -> Some (targetUri, sym)
701-
| None -> Some (uri, sym)
702-
| None ->
703-
match wordAtCursor with
704-
| Some word ->
705-
documents
706-
|> Seq.tryPick (fun kv ->
707-
kv.Value.Symbols
708-
|> List.tryFind (fun s -> s.Name = word)
709-
|> Option.map (fun s -> kv.Key, s))
710-
| None -> None
711-
712-
match symbolAndUri with
713-
| Some (targetUri, sym) ->
693+
match tryResolveLocalBindingAtPosition doc line character with
694+
| Some localBinding ->
714695
let loc = JsonObject()
696+
let targetUri =
697+
tryUriFromSpanFile uri localBinding.DeclSpan
698+
|> Option.defaultValue uri
715699
loc["uri"] <- JsonValue.Create(targetUri)
716-
loc["range"] <- toLspRange sym.Span
700+
loc["range"] <- toLspRange localBinding.DeclSpan
717701
LspProtocol.sendResponse idNode (Some loc)
718702
| None ->
719-
let injectedDefinition =
720-
match wordAtCursor with
721-
| Some word ->
722-
let candidates =
723-
if word.Contains('.') then
724-
[ word; word.Split('.') |> Array.last ]
725-
else
726-
[ word ]
727-
728-
candidates
729-
|> List.tryPick (fun candidate ->
730-
doc.InjectedFunctionDefinitions
731-
|> Map.tryFind candidate
732-
|> Option.map (fun target -> candidate, target))
703+
let localSymbol = tryResolveSymbol doc line character
704+
let wordAtCursor = tryGetWordAtPosition doc.Text line character
705+
706+
let symbolAndUri =
707+
match localSymbol with
708+
| Some sym ->
709+
match tryUriFromSpanFile uri sym.Span with
710+
| Some targetUri -> Some (targetUri, sym)
711+
| None -> Some (uri, sym)
733712
| None ->
734-
None
735-
736-
match injectedDefinition with
737-
| Some (_, (targetUri, targetSpan)) ->
713+
match wordAtCursor with
714+
| Some word ->
715+
documents
716+
|> Seq.tryPick (fun kv ->
717+
kv.Value.Symbols
718+
|> List.tryFind (fun s -> s.Name = word)
719+
|> Option.map (fun s -> kv.Key, s))
720+
| None -> None
721+
722+
match symbolAndUri with
723+
| Some (targetUri, sym) ->
738724
let loc = JsonObject()
739725
loc["uri"] <- JsonValue.Create(targetUri)
740-
loc["range"] <- toLspRange targetSpan
726+
loc["range"] <- toLspRange sym.Span
741727
LspProtocol.sendResponse idNode (Some loc)
742728
| None ->
743-
match tryResolveTypeTargetAtPosition doc line character with
744-
| Some typeName ->
745-
match doc.Symbols |> List.tryFind (fun s -> s.Kind = 5 && s.Name = typeName) with
746-
| Some typeSym ->
747-
let loc = JsonObject()
748-
let targetUri =
749-
tryUriFromSpanFile uri typeSym.Span
750-
|> Option.defaultValue uri
751-
loc["uri"] <- JsonValue.Create(targetUri)
752-
loc["range"] <- toLspRange typeSym.Span
753-
LspProtocol.sendResponse idNode (Some loc)
729+
let injectedDefinition =
730+
match wordAtCursor with
731+
| Some word ->
732+
let candidates =
733+
if word.Contains('.') then
734+
[ word; word.Split('.') |> Array.last ]
735+
else
736+
[ word ]
737+
738+
candidates
739+
|> List.tryPick (fun candidate ->
740+
doc.InjectedFunctionDefinitions
741+
|> Map.tryFind candidate
742+
|> Option.map (fun target -> candidate, target))
754743
| None ->
755-
match tryFindInjectedTypeDefinition typeName with
756-
| Some (targetUri, targetSpan) ->
744+
None
745+
746+
match injectedDefinition with
747+
| Some (_, (targetUri, targetSpan)) ->
748+
let loc = JsonObject()
749+
loc["uri"] <- JsonValue.Create(targetUri)
750+
loc["range"] <- toLspRange targetSpan
751+
LspProtocol.sendResponse idNode (Some loc)
752+
| None ->
753+
match tryResolveTypeTargetAtPosition doc line character with
754+
| Some typeName ->
755+
match doc.Symbols |> List.tryFind (fun s -> s.Kind = 5 && s.Name = typeName) with
756+
| Some typeSym ->
757757
let loc = JsonObject()
758+
let targetUri =
759+
tryUriFromSpanFile uri typeSym.Span
760+
|> Option.defaultValue uri
758761
loc["uri"] <- JsonValue.Create(targetUri)
759-
loc["range"] <- toLspRange targetSpan
762+
loc["range"] <- toLspRange typeSym.Span
760763
LspProtocol.sendResponse idNode (Some loc)
761764
| None ->
762-
LspProtocol.sendResponse idNode None
763-
| None ->
764-
LspProtocol.sendResponse idNode None
765+
match tryFindInjectedTypeDefinition typeName with
766+
| Some (targetUri, targetSpan) ->
767+
let loc = JsonObject()
768+
loc["uri"] <- JsonValue.Create(targetUri)
769+
loc["range"] <- toLspRange targetSpan
770+
LspProtocol.sendResponse idNode (Some loc)
771+
| None ->
772+
LspProtocol.sendResponse idNode None
773+
| None ->
774+
LspProtocol.sendResponse idNode None
765775
| _ -> LspProtocol.sendResponse idNode None
766776

767777
let handleTypeDefinition (idNode: JsonNode) (paramsObj: JsonObject) =

src/FScript.CSharpInterop/LanguageServer/LspModel.fs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,17 @@ module LspModel =
5050
TypeTargetName: string option
5151
Span: Span }
5252

53+
type LocalBindingKind =
54+
| Parameter
55+
| LetBound
56+
| PatternBound
57+
| LoopBound
58+
5359
type LocalBindingInfo =
5460
{ Name: string
5561
DeclSpan: Span
5662
ScopeSpan: Span
63+
BindingKind: LocalBindingKind
5764
AnnotationType: string option }
5865

5966
type DocumentState =

0 commit comments

Comments
 (0)