Skip to content

Commit 8bf4e4a

Browse files
authored
Add support for inlay hints (#907)
* add inlay hints based on Phillip's work * Add tests
1 parent d849918 commit 8bf4e4a

10 files changed

Lines changed: 372 additions & 13 deletions

File tree

src/FsAutoComplete.Core/Commands.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,6 +1600,9 @@ type Commands
16001600
// return CoreResponse.Res html
16011601
// }
16021602

1603+
member _.InlayHints (text, tyRes: ParseAndCheckResults, range) =
1604+
FsAutoComplete.Core.InlayHints.provideHints(text, tyRes, range)
1605+
16031606
member __.PipelineHints(tyRes: ParseAndCheckResults) =
16041607
result {
16051608
let! contents = state.TryGetFileSource tyRes.FileName

src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<Compile Include="Fsdn.fs" />
4343
<!-- <Compile Include="Lint.fs" /> -->
4444
<Compile Include="SignatureHelp.fs" />
45+
<Compile Include="InlayHints.fs" />
4546
<Compile Include="Commands.fs" />
4647
</ItemGroup>
4748
<Import Project="..\..\.paket\Paket.Restore.targets" />
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
module FsAutoComplete.Core.InlayHints
2+
3+
open System
4+
open FSharp.Compiler.Text
5+
open FSharp.Compiler.Syntax
6+
open FsToolkit.ErrorHandling
7+
open FsAutoComplete
8+
open FSharp.Compiler.Symbols
9+
open FSharp.UMX
10+
open System.Linq
11+
open System.Collections.Immutable
12+
open FSharp.Compiler.CodeAnalysis
13+
open System.Text
14+
15+
type HintKind = Parameter | Type
16+
type Hint = { Text: string; Pos: Position; Kind: HintKind }
17+
18+
let private getArgumentsFor (state: FsAutoComplete.State, p: ParseAndCheckResults, identText: Range) =
19+
option {
20+
21+
let! contents =
22+
state.TryGetFileSource p.FileName
23+
|> Option.ofResult
24+
25+
let! line = contents.GetLine identText.End
26+
let! symbolUse = p.TryGetSymbolUse identText.End line
27+
28+
match symbolUse.Symbol with
29+
| :? FSharpMemberOrFunctionOrValue as mfv when
30+
mfv.IsFunction
31+
|| mfv.IsConstructor
32+
|| mfv.CurriedParameterGroups.Count <> 0
33+
->
34+
let parameters = mfv.CurriedParameterGroups
35+
36+
let formatted =
37+
parameters
38+
|> Seq.collect (fun pGroup -> pGroup |> Seq.map (fun p -> p.DisplayName + ":"))
39+
40+
return formatted |> Array.ofSeq
41+
| _ -> return! None
42+
}
43+
44+
let private isSignatureFile (f: string<LocalPath>) =
45+
System.IO.Path.GetExtension(UMX.untag f) = ".fsi"
46+
47+
let getFirstPositionAfterParen (str: string) startPos =
48+
match str with
49+
| null -> -1
50+
| str when startPos > str.Length -> -1
51+
| str -> str.IndexOf('(') + 1
52+
53+
let provideHints (text: NamedText, p: ParseAndCheckResults, range: Range) : Hint [] =
54+
let parseFileResults, checkFileResults = p.GetParseResults, p.GetCheckResults
55+
56+
let symbolUses =
57+
checkFileResults.GetAllUsesOfAllSymbolsInFile(System.Threading.CancellationToken.None)
58+
|> Seq.filter (fun su -> Range.rangeContainsRange range su.Range)
59+
|> Seq.toList
60+
61+
let typeHints = ImmutableArray.CreateBuilder()
62+
let parameterHints = ImmutableArray.CreateBuilder()
63+
64+
let isValidForTypeHint (funcOrValue: FSharpMemberOrFunctionOrValue) (symbolUse: FSharpSymbolUse) =
65+
let isLambdaIfFunction =
66+
funcOrValue.IsFunction
67+
&& parseFileResults.IsBindingALambdaAtPosition symbolUse.Range.Start
68+
69+
(funcOrValue.IsValue || isLambdaIfFunction)
70+
&& not (parseFileResults.IsTypeAnnotationGivenAtPosition symbolUse.Range.Start)
71+
&& symbolUse.IsFromDefinition
72+
&& not funcOrValue.IsMember
73+
&& not funcOrValue.IsMemberThisValue
74+
&& not funcOrValue.IsConstructorThisValue
75+
&& not (PrettyNaming.IsOperatorDisplayName funcOrValue.DisplayName)
76+
77+
for symbolUse in symbolUses do
78+
match symbolUse.Symbol with
79+
| :? FSharpMemberOrFunctionOrValue as funcOrValue when isValidForTypeHint funcOrValue symbolUse ->
80+
let layout =
81+
": "
82+
+ funcOrValue.ReturnParameter.Type.Format symbolUse.DisplayContext
83+
84+
let hint =
85+
{ Text = layout
86+
Pos = symbolUse.Range.End
87+
Kind = Type }
88+
89+
typeHints.Add(hint)
90+
91+
| :? FSharpMemberOrFunctionOrValue as func when func.IsFunction && not symbolUse.IsFromDefinition ->
92+
let appliedArgRangesOpt =
93+
parseFileResults.GetAllArgumentsForFunctionApplicationAtPostion symbolUse.Range.Start
94+
95+
match appliedArgRangesOpt with
96+
| None -> ()
97+
| Some [] -> ()
98+
| Some appliedArgRanges ->
99+
let parameters = func.CurriedParameterGroups |> Seq.concat
100+
let appliedArgRanges = appliedArgRanges |> Array.ofList
101+
let definitionArgs = parameters |> Array.ofSeq
102+
103+
for idx = 0 to appliedArgRanges.Length - 1 do
104+
let appliedArgRange = appliedArgRanges.[idx]
105+
let definitionArgName = definitionArgs.[idx].DisplayName
106+
107+
if not (String.IsNullOrWhiteSpace(definitionArgName)) then
108+
let hint =
109+
{ Text = definitionArgName + " ="
110+
Pos = appliedArgRange.Start
111+
Kind = Parameter }
112+
113+
parameterHints.Add(hint)
114+
115+
| :? FSharpMemberOrFunctionOrValue as methodOrConstructor when methodOrConstructor.IsConstructor -> // TODO: support methods when this API comes into FCS
116+
let endPosForMethod = symbolUse.Range.End
117+
let line, _ = Position.toZ endPosForMethod
118+
119+
let afterParenPosInLine =
120+
getFirstPositionAfterParen (text.Lines.[line].ToString()) (endPosForMethod.Column)
121+
122+
let tupledParamInfos =
123+
parseFileResults.FindParameterLocations(Position.fromZ line afterParenPosInLine)
124+
125+
let appliedArgRanges =
126+
parseFileResults.GetAllArgumentsForFunctionApplicationAtPostion symbolUse.Range.Start
127+
128+
match tupledParamInfos, appliedArgRanges with
129+
| None, None -> ()
130+
131+
// Prefer looking at the "tupled" view if it exists, even if the other ranges exist.
132+
// M(1, 2) can give results for both, but in that case we want the "tupled" view.
133+
| Some tupledParamInfos, _ ->
134+
let parameters =
135+
methodOrConstructor.CurriedParameterGroups
136+
|> Seq.concat
137+
|> Array.ofSeq // TODO: need ArgumentLocations to be surfaced
138+
139+
for idx = 0 to parameters.Length - 1 do
140+
// let paramLocationInfo = tupledParamInfos. .ArgumentLocations.[idx]
141+
// let paramName = parameters.[idx].DisplayName
142+
// if not paramLocationInfo.IsNamedArgument && not (String.IsNullOrWhiteSpace(paramName)) then
143+
// let hint = { Text = paramName + " ="; Pos = paramLocationInfo.ArgumentRange.Start; Kind = Parameter }
144+
// parameterHints.Add(hint)
145+
()
146+
147+
// This will only happen for curried methods defined in F#.
148+
| _, Some appliedArgRanges ->
149+
let parameters =
150+
methodOrConstructor.CurriedParameterGroups
151+
|> Seq.concat
152+
153+
let appliedArgRanges = appliedArgRanges |> Array.ofList
154+
let definitionArgs = parameters |> Array.ofSeq
155+
156+
for idx = 0 to appliedArgRanges.Length - 1 do
157+
let appliedArgRange = appliedArgRanges.[idx]
158+
let definitionArgName = definitionArgs.[idx].DisplayName
159+
160+
if not (String.IsNullOrWhiteSpace(definitionArgName)) then
161+
let hint =
162+
{ Text = definitionArgName + " ="
163+
Pos = appliedArgRange.Start
164+
Kind = Parameter }
165+
166+
parameterHints.Add(hint)
167+
| _ -> ()
168+
169+
let typeHints = typeHints.ToImmutableArray()
170+
let parameterHints = parameterHints.ToImmutableArray()
171+
172+
typeHints.AddRange(parameterHints).ToArray()

src/FsAutoComplete/FsAutoComplete.Lsp.fs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ type OptionallyVersionedTextDocumentPositionParams =
4444
member this.TextDocument with get() = { Uri = this.TextDocument.Uri }
4545
member this.Position with get() = this.Position
4646

47+
[<RequireQualifiedAccess>]
48+
type InlayHintKind = Type | Parameter
49+
50+
type LSPInlayHint = {
51+
Text : string
52+
Pos : Types.Position
53+
Kind : InlayHintKind
54+
}
55+
4756
module Result =
4857
let ofCoreResponse (r: CoreResponse<'a>) =
4958
match r with
@@ -662,7 +671,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS
662671

663672
///Helper function for handling file requests using **recent** type check results
664673
member x.fileHandler<'a>
665-
(f: string<LocalPath> -> ParseAndCheckResults -> ISourceText -> AsyncLspResult<'a>)
674+
(f: string<LocalPath> -> ParseAndCheckResults -> NamedText -> AsyncLspResult<'a>)
666675
(file: string<LocalPath>)
667676
: AsyncLspResult<'a> =
668677
async {
@@ -2652,6 +2661,32 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS
26522661
// return res
26532662
// }
26542663

2664+
member x.FSharpInlayHints(p: LspHelpers.FSharpInlayHintsRequest) =
2665+
let mapHintKind (k: FsAutoComplete.Core.InlayHints.HintKind): InlayHintKind =
2666+
match k with
2667+
| FsAutoComplete.Core.InlayHints.HintKind.Type -> InlayHintKind.Type
2668+
| FsAutoComplete.Core.InlayHints.HintKind.Parameter -> InlayHintKind.Parameter
2669+
2670+
logger.info (
2671+
Log.setMessage "FSharpInlayHints Request: {parms}"
2672+
>> Log.addContextDestructured "parms" p
2673+
)
2674+
2675+
let fn = p.TextDocument.GetFilePath() |> Utils.normalizePath
2676+
let fcsRange = protocolRangeToRange (UMX.untag fn) p.Range
2677+
fn
2678+
|> x.fileHandler (fun fn tyRes lines ->
2679+
let hints = commands.InlayHints(lines, tyRes, fcsRange)
2680+
let lspHints =
2681+
hints
2682+
|> Array.map (fun h -> {
2683+
Text = h.Text
2684+
Pos = fcsPosToLsp h.Pos
2685+
Kind = mapHintKind h.Kind
2686+
})
2687+
AsyncLspResult.success lspHints
2688+
)
2689+
26552690
member x.FSharpPipelineHints(p: FSharpPipelineHintRequest) =
26562691
logger.info (
26572692
Log.setMessage "FSharpPipelineHints Request: {parms}"
@@ -2705,6 +2740,7 @@ let startCore backgroundServiceEnabled toolsPath workspaceLoaderFactory =
27052740
|> Map.add "fsproj/addFileAbove" (requestHandling (fun s p -> s.FsProjAddFileAbove(p)))
27062741
|> Map.add "fsproj/addFileBelow" (requestHandling (fun s p -> s.FsProjAddFileBelow(p)))
27072742
|> Map.add "fsproj/addFile" (requestHandling (fun s p -> s.FsProjAddFile(p)))
2743+
|> Map.add "fsharp/inlayHints" (requestHandling (fun s p -> s.FSharpInlayHints(p)))
27082744

27092745
let state =
27102746
State.Initial toolsPath workspaceLoaderFactory

src/FsAutoComplete/LspHelpers.fs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,3 +848,9 @@ let encodeSemanticHighlightRanges (rangesAndHighlights: (struct(Ionide.LanguageS
848848
prev <- currentRange
849849
idx <- idx + 5
850850
Some finalArray
851+
852+
853+
type FSharpInlayHintsRequest = {
854+
TextDocument: TextDocumentIdentifier
855+
Range: Range
856+
}

test/FsAutoComplete.Tests.Lsp/Helpers.fs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ let logger = Expecto.Logging.Log.create "LSPTests"
115115
type Cacher<'t> = System.Reactive.Subjects.ReplaySubject<'t>
116116
type ClientEvents = IObservable<string * obj>
117117

118+
module Range =
119+
let rangeContainsPos (range : Range) (pos : Position) =
120+
range.Start <= pos && pos <= range.End
121+
118122
let record (cacher: Cacher<_>) =
119123
fun name payload ->
120124
cacher.OnNext (name, payload);
@@ -482,8 +486,17 @@ let waitForTestDetected (fileName: string) (events: ClientEvents): Async<TestDet
482486
testNotificationFileName = fileName)
483487
|> Async.AwaitObservable
484488

485-
486489
let waitForEditsForFile file =
487490
workspaceEdits
488491
>> editsFor file
489492
>> Async.AwaitObservable
493+
494+
let trySerialize (t: string): 't option =
495+
try
496+
JsonSerializer.readJson t |> Some
497+
with _ -> None
498+
499+
let (|As|_|) (m: PlainNotification): 't option =
500+
match trySerialize m.Content with
501+
| Some(r: FsAutoComplete.CommandResponse.ResponseMsg<'t>) -> Some r.Data
502+
| None -> None

test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,9 @@ open Expecto
44
open System.IO
55
open Ionide.LanguageServerProtocol.Types
66
open FsAutoComplete
7-
open FsAutoComplete.LspHelpers
87
open Helpers
98
open FsToolkit.ErrorHandling
109

11-
let trySerialize (t: string): 't option =
12-
try
13-
JsonSerializer.readJson t |> Some
14-
with _ -> None
15-
16-
let (|As|_|) (m: PlainNotification): 't option =
17-
match trySerialize m.Content with
18-
| Some(r: FsAutoComplete.CommandResponse.ResponseMsg<'t>) -> Some r.Data
19-
| None -> None
20-
2110
let docFormattingTest state =
2211
let server =
2312
async {

0 commit comments

Comments
 (0)