Skip to content

Commit b4ebc0d

Browse files
committed
Add local-variable hover types and 0.28.0 changelog
1 parent 98c0f9c commit b4ebc0d

10 files changed

Lines changed: 405 additions & 117 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ obj/
1414
.vs/
1515
.vscode/*
1616
!.vscode/launch.json
17+
!.vscode/tasks.json
1718
vscode-fscript/node_modules/
1819

1920
# Rider

.vscode/launch.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
],
1212
"outFiles": [
1313
"${workspaceFolder}/vscode-fscript/**/*.js"
14-
]
14+
],
15+
"preLaunchTask": "fscript: compile extension"
1516
}
1617
]
1718
}

.vscode/tasks.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "fscript: compile extension",
6+
"type": "shell",
7+
"command": "npm run compile",
8+
"options": {
9+
"cwd": "${workspaceFolder}/vscode-fscript"
10+
},
11+
"problemMatcher": []
12+
}
13+
]
14+
}

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to FScript are documented in this file.
44

5+
## [0.28.0]
6+
7+
- Added local variable type hover in LSP.
8+
- Added local variable type capture in type inference.
9+
- Added VS Code extension auto-compile for development startup.
10+
511
## [0.27.0]
612

713
- Added structural update syntax support with `with` inside structural literals: `{| base with Field = value |}`.

src/FScript.Language/TypeInfer.fs

Lines changed: 258 additions & 101 deletions
Large diffs are not rendered by default.

src/FScript.LanguageServer/LspHandlers.fs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -355,24 +355,33 @@ module LspHandlers =
355355
result["contents"] <- contents
356356
LspProtocol.sendResponse idNode (Some result)
357357
| None ->
358-
match tryResolveSymbol doc line character with
359-
| Some sym ->
360-
let signature =
361-
match sym.TypeText with
362-
| Some t -> $"{sym.Name} : {t}"
363-
| None -> sym.Name
364-
358+
match tryGetLocalVariableHoverInfo doc line character with
359+
| Some (name, typeText) ->
365360
let contents = JsonObject()
366361
contents["kind"] <- JsonValue.Create("markdown")
367-
let kindLine = symbolKindLabel sym.Kind
368-
let locationLine = $"defined at L{sym.Span.Start.Line}:C{sym.Span.Start.Column}"
369-
contents["value"] <- JsonValue.Create($"```fscript\n{signature}\n```\n{kindLine}\n\n{locationLine}")
370-
362+
contents["value"] <- JsonValue.Create($"```fscript\n{name} : {typeText}\n```\nlocal-variable")
371363
let result = JsonObject()
372364
result["contents"] <- contents
373365
LspProtocol.sendResponse idNode (Some result)
374366
| None ->
375-
LspProtocol.sendResponse idNode None
367+
match tryResolveSymbol doc line character with
368+
| Some sym ->
369+
let signature =
370+
match sym.TypeText with
371+
| Some t -> $"{sym.Name} : {t}"
372+
| None -> sym.Name
373+
374+
let contents = JsonObject()
375+
contents["kind"] <- JsonValue.Create("markdown")
376+
let kindLine = symbolKindLabel sym.Kind
377+
let locationLine = $"defined at L{sym.Span.Start.Line}:C{sym.Span.Start.Column}"
378+
contents["value"] <- JsonValue.Create($"```fscript\n{signature}\n```\n{kindLine}\n\n{locationLine}")
379+
380+
let result = JsonObject()
381+
result["contents"] <- contents
382+
LspProtocol.sendResponse idNode (Some result)
383+
| None ->
384+
LspProtocol.sendResponse idNode None
376385
| _ -> LspProtocol.sendResponse idNode None
377386

378387

src/FScript.LanguageServer/LspModel.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ module LspModel =
5757
ParameterTypeTargets: Map<string, string>
5858
FunctionParameters: Map<string, string list>
5959
ParameterTypeHints: (Span * string) list
60+
LocalVariableTypeHints: (Span * string * string) list
6061
// Variable occurrences keyed by identifier, sourced from AST spans.
6162
// This avoids text-based false positives (for example record field labels).
6263
VariableOccurrences: Map<string, Span list> }

src/FScript.LanguageServer/LspSymbols.fs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ module LspSymbols =
459459
let mutable parameterTypeTargets : Map<string, string> = Map.empty
460460
let mutable functionParameters : Map<string, string list> = Map.empty
461461
let mutable parameterTypeHints : (Span * string) list = []
462+
let mutable localVariableTypeHints : (Span * string * string) list = []
462463

463464
let mutable parsedProgram : Program option = None
464465

@@ -474,9 +475,16 @@ module LspSymbols =
474475
parameterTypeHints <- buildParameterTypeHints program None
475476
else
476477
try
477-
let typed = TypeInfer.inferProgram program
478+
let typed, localTypes = TypeInfer.inferProgramWithLocalVariableTypes program
478479
symbols <- buildSymbolsFromProgram program (Some typed)
479480
parameterTypeHints <- buildParameterTypeHints program (Some typed)
481+
localVariableTypeHints <-
482+
localTypes
483+
|> List.filter (fun entry ->
484+
match entry.Span.Start.File with
485+
| Some file -> String.Equals(file, sourceName, StringComparison.OrdinalIgnoreCase)
486+
| None -> true)
487+
|> List.map (fun entry -> entry.Span, entry.Name, Types.typeToString entry.Type)
480488
with
481489
| TypeException err ->
482490
diagnostics.Add(diagnostic 1 "type" err.Span err.Message)
@@ -504,7 +512,7 @@ module LspSymbols =
504512
diagnostics.Add(diagnostic 2 "unused" span $"Unused top-level binding '{name}'")
505513
| None -> ()
506514

507-
documents[uri] <- { Text = text; Symbols = symbols; RecordParameterFields = recordParamFields; ParameterTypeTargets = parameterTypeTargets; FunctionParameters = functionParameters; ParameterTypeHints = parameterTypeHints; VariableOccurrences = occurrences }
515+
documents[uri] <- { Text = text; Symbols = symbols; RecordParameterFields = recordParamFields; ParameterTypeTargets = parameterTypeTargets; FunctionParameters = functionParameters; ParameterTypeHints = parameterTypeHints; LocalVariableTypeHints = localVariableTypeHints; VariableOccurrences = occurrences }
508516
publishDiagnostics uri (diagnostics |> Seq.toList)
509517

510518
let tryResolveSymbol (doc: DocumentState) (line: int) (character: int) : TopLevelSymbol option =
@@ -821,6 +829,22 @@ module LspSymbols =
821829
| _ ->
822830
None
823831

832+
let private spanContainsPosition (span: Span) (line: int) (character: int) =
833+
let line1 = line + 1
834+
let col1 = character + 1
835+
let startsBefore =
836+
line1 > span.Start.Line
837+
|| (line1 = span.Start.Line && col1 >= span.Start.Column)
838+
let endsAfter =
839+
line1 < span.End.Line
840+
|| (line1 = span.End.Line && col1 <= span.End.Column)
841+
startsBefore && endsAfter
842+
843+
let tryGetLocalVariableHoverInfo (doc: DocumentState) (line: int) (character: int) : (string * string) option =
844+
doc.LocalVariableTypeHints
845+
|> List.tryFind (fun (span, _, _) -> spanContainsPosition span line character)
846+
|> Option.map (fun (_, name, typeText) -> name, typeText)
847+
824848
let private tryMemberCompletionItems (doc: DocumentState) (prefix: string option) =
825849
match prefix with
826850
| Some p when p.Contains('.') ->

tests/FScript.LanguageServer.Tests/LspServerTests.fs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2616,6 +2616,80 @@ type LspServerTests () =
26162616
try shutdown client with _ -> ()
26172617
LspClient.stop client
26182618

2619+
[<Test>]
2620+
member _.``Hover returns inferred type for local lambda variables`` () =
2621+
let client = LspClient.start ()
2622+
try
2623+
initialize client
2624+
2625+
let uri = "file:///tmp/hover-local-variables-test.fss"
2626+
let source =
2627+
"let rec fib n = if n < 2 then n else fib (n - 1) + fib (n - 2)\n"
2628+
+ "let values =\n"
2629+
+ " [0..9]\n"
2630+
+ " |> List.map (fun i ->\n"
2631+
+ " i |> fib |> fun x ->\n"
2632+
+ " $\"{x}\")\n"
2633+
2634+
let td = JsonObject()
2635+
td["uri"] <- JsonValue.Create(uri)
2636+
td["languageId"] <- JsonValue.Create("fscript")
2637+
td["version"] <- JsonValue.Create(1)
2638+
td["text"] <- JsonValue.Create(source)
2639+
2640+
let didOpenParams = JsonObject()
2641+
didOpenParams["textDocument"] <- td
2642+
LspClient.sendNotification client "textDocument/didOpen" (Some didOpenParams)
2643+
2644+
LspClient.readUntil client 10000 (fun msg ->
2645+
match msg["method"] with
2646+
| :? JsonValue as mv ->
2647+
try mv.GetValue<string>() = "textDocument/publishDiagnostics" with _ -> false
2648+
| _ -> false)
2649+
|> ignore
2650+
2651+
let requestHover (requestId: int) (line: int) (character: int) =
2652+
let hoverParams = JsonObject()
2653+
let textDocument = JsonObject()
2654+
textDocument["uri"] <- JsonValue.Create(uri)
2655+
let position = JsonObject()
2656+
position["line"] <- JsonValue.Create(line)
2657+
position["character"] <- JsonValue.Create(character)
2658+
hoverParams["textDocument"] <- textDocument
2659+
hoverParams["position"] <- position
2660+
LspClient.sendRequest client requestId "textDocument/hover" (Some hoverParams)
2661+
LspClient.readUntil client 10000 (fun msg ->
2662+
match msg["id"] with
2663+
| :? JsonValue as idv ->
2664+
try idv.GetValue<int>() = requestId with _ -> false
2665+
| _ -> false)
2666+
2667+
let hoverText (resp: JsonNode) =
2668+
match resp["result"] with
2669+
| :? JsonObject as result ->
2670+
match result["contents"] with
2671+
| :? JsonObject as contents ->
2672+
match contents["value"] with
2673+
| :? JsonValue as value -> value.GetValue<string>()
2674+
| _ -> ""
2675+
| _ -> ""
2676+
| _ -> ""
2677+
2678+
let hasLocalHoverType (text: string) (name: string) (typeText: string) =
2679+
text.Contains($"{name} : {typeText}", StringComparison.Ordinal)
2680+
&& text.Contains("local-variable", StringComparison.Ordinal)
2681+
2682+
let hoverI = requestHover 41 3 21
2683+
let hoverX = requestHover 42 4 24
2684+
let hoverIText = hoverText hoverI
2685+
let hoverXText = hoverText hoverX
2686+
2687+
Assert.That(hasLocalHoverType hoverIText "i" "int", Is.True, $"Unexpected hover for i: {hoverIText}")
2688+
Assert.That(hasLocalHoverType hoverXText "x" "int", Is.True, $"Unexpected hover for x: {hoverXText}")
2689+
finally
2690+
try shutdown client with _ -> ()
2691+
LspClient.stop client
2692+
26192693
[<Test>]
26202694
member _.``Rename returns workspace edit for all symbol occurrences`` () =
26212695
let client = LspClient.start ()

vscode-fscript/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@
9292
"lint": "echo \"No lint configured\"",
9393
"compile": "esbuild extension.js --bundle --platform=node --format=cjs --target=node20 --outfile=dist/extension.js --external:vscode",
9494
"vscode:prepublish": "npm run compile",
95-
"package": "vsce package"
95+
"package": "vsce package",
96+
"postinstall": "npm run compile"
9697
},
9798
"dependencies": {
9899
"vscode-languageclient": "^9.0.1"

0 commit comments

Comments
 (0)