Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/FsAutoComplete.Core/Commands.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1600,6 +1600,9 @@ type Commands
// return CoreResponse.Res html
// }

member _.InlayHints (text, tyRes: ParseAndCheckResults, range) =
FsAutoComplete.Core.InlayHints.provideHints(text, tyRes, range)

member __.PipelineHints(tyRes: ParseAndCheckResults) =
result {
let! contents = state.TryGetFileSource tyRes.FileName
Expand Down
1 change: 1 addition & 0 deletions src/FsAutoComplete.Core/FsAutoComplete.Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<Compile Include="Fsdn.fs" />
<!-- <Compile Include="Lint.fs" /> -->
<Compile Include="SignatureHelp.fs" />
<Compile Include="InlayHints.fs" />
<Compile Include="Commands.fs" />
</ItemGroup>
<Import Project="..\..\.paket\Paket.Restore.targets" />
Expand Down
172 changes: 172 additions & 0 deletions src/FsAutoComplete.Core/InlayHints.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
module FsAutoComplete.Core.InlayHints

open System
open FSharp.Compiler.Text
open FSharp.Compiler.Syntax
open FsToolkit.ErrorHandling
open FsAutoComplete
open FSharp.Compiler.Symbols
open FSharp.UMX
open System.Linq
open System.Collections.Immutable
open FSharp.Compiler.CodeAnalysis
open System.Text

type HintKind = Parameter | Type
type Hint = { Text: string; Pos: Position; Kind: HintKind }

let private getArgumentsFor (state: FsAutoComplete.State, p: ParseAndCheckResults, identText: Range) =
option {

let! contents =
state.TryGetFileSource p.FileName
|> Option.ofResult

let! line = contents.GetLine identText.End
let! symbolUse = p.TryGetSymbolUse identText.End line

match symbolUse.Symbol with
| :? FSharpMemberOrFunctionOrValue as mfv when
mfv.IsFunction
|| mfv.IsConstructor
|| mfv.CurriedParameterGroups.Count <> 0
->
let parameters = mfv.CurriedParameterGroups

let formatted =
parameters
|> Seq.collect (fun pGroup -> pGroup |> Seq.map (fun p -> p.DisplayName + ":"))

return formatted |> Array.ofSeq
| _ -> return! None
}

let private isSignatureFile (f: string<LocalPath>) =
System.IO.Path.GetExtension(UMX.untag f) = ".fsi"

let getFirstPositionAfterParen (str: string) startPos =
match str with
| null -> -1
| str when startPos > str.Length -> -1
| str -> str.IndexOf('(') + 1

let provideHints (text: NamedText, p: ParseAndCheckResults, range: Range) : Hint [] =
let parseFileResults, checkFileResults = p.GetParseResults, p.GetCheckResults

let symbolUses =
checkFileResults.GetAllUsesOfAllSymbolsInFile(System.Threading.CancellationToken.None)
|> Seq.filter (fun su -> Range.rangeContainsRange range su.Range)
|> Seq.toList

let typeHints = ImmutableArray.CreateBuilder()
let parameterHints = ImmutableArray.CreateBuilder()

let isValidForTypeHint (funcOrValue: FSharpMemberOrFunctionOrValue) (symbolUse: FSharpSymbolUse) =
let isLambdaIfFunction =
funcOrValue.IsFunction
&& parseFileResults.IsBindingALambdaAtPosition symbolUse.Range.Start

(funcOrValue.IsValue || isLambdaIfFunction)
&& not (parseFileResults.IsTypeAnnotationGivenAtPosition symbolUse.Range.Start)
&& symbolUse.IsFromDefinition
&& not funcOrValue.IsMember
&& not funcOrValue.IsMemberThisValue
&& not funcOrValue.IsConstructorThisValue
&& not (PrettyNaming.IsOperatorDisplayName funcOrValue.DisplayName)

for symbolUse in symbolUses do
match symbolUse.Symbol with
| :? FSharpMemberOrFunctionOrValue as funcOrValue when isValidForTypeHint funcOrValue symbolUse ->
let layout =
": "
+ funcOrValue.ReturnParameter.Type.Format symbolUse.DisplayContext

let hint =
{ Text = layout
Pos = symbolUse.Range.End
Kind = Type }

typeHints.Add(hint)

| :? FSharpMemberOrFunctionOrValue as func when func.IsFunction && not symbolUse.IsFromDefinition ->
let appliedArgRangesOpt =
parseFileResults.GetAllArgumentsForFunctionApplicationAtPostion symbolUse.Range.Start

match appliedArgRangesOpt with
| None -> ()
| Some [] -> ()
| Some appliedArgRanges ->
let parameters = func.CurriedParameterGroups |> Seq.concat
let appliedArgRanges = appliedArgRanges |> Array.ofList
let definitionArgs = parameters |> Array.ofSeq

for idx = 0 to appliedArgRanges.Length - 1 do
let appliedArgRange = appliedArgRanges.[idx]
let definitionArgName = definitionArgs.[idx].DisplayName

if not (String.IsNullOrWhiteSpace(definitionArgName)) then
let hint =
{ Text = definitionArgName + " ="
Pos = appliedArgRange.Start
Kind = Parameter }

parameterHints.Add(hint)

| :? FSharpMemberOrFunctionOrValue as methodOrConstructor when methodOrConstructor.IsConstructor -> // TODO: support methods when this API comes into FCS
let endPosForMethod = symbolUse.Range.End
let line, _ = Position.toZ endPosForMethod

let afterParenPosInLine =
getFirstPositionAfterParen (text.Lines.[line].ToString()) (endPosForMethod.Column)

let tupledParamInfos =
parseFileResults.FindParameterLocations(Position.fromZ line afterParenPosInLine)

let appliedArgRanges =
parseFileResults.GetAllArgumentsForFunctionApplicationAtPostion symbolUse.Range.Start

match tupledParamInfos, appliedArgRanges with
| None, None -> ()

// Prefer looking at the "tupled" view if it exists, even if the other ranges exist.
// M(1, 2) can give results for both, but in that case we want the "tupled" view.
| Some tupledParamInfos, _ ->
let parameters =
methodOrConstructor.CurriedParameterGroups
|> Seq.concat
|> Array.ofSeq // TODO: need ArgumentLocations to be surfaced

for idx = 0 to parameters.Length - 1 do
// let paramLocationInfo = tupledParamInfos. .ArgumentLocations.[idx]
// let paramName = parameters.[idx].DisplayName
// if not paramLocationInfo.IsNamedArgument && not (String.IsNullOrWhiteSpace(paramName)) then
// let hint = { Text = paramName + " ="; Pos = paramLocationInfo.ArgumentRange.Start; Kind = Parameter }
// parameterHints.Add(hint)
()

// This will only happen for curried methods defined in F#.
| _, Some appliedArgRanges ->
let parameters =
methodOrConstructor.CurriedParameterGroups
|> Seq.concat

let appliedArgRanges = appliedArgRanges |> Array.ofList
let definitionArgs = parameters |> Array.ofSeq

for idx = 0 to appliedArgRanges.Length - 1 do
let appliedArgRange = appliedArgRanges.[idx]
let definitionArgName = definitionArgs.[idx].DisplayName

if not (String.IsNullOrWhiteSpace(definitionArgName)) then
let hint =
{ Text = definitionArgName + " ="
Pos = appliedArgRange.Start
Kind = Parameter }

parameterHints.Add(hint)
| _ -> ()

let typeHints = typeHints.ToImmutableArray()
let parameterHints = parameterHints.ToImmutableArray()

typeHints.AddRange(parameterHints).ToArray()
38 changes: 37 additions & 1 deletion src/FsAutoComplete/FsAutoComplete.Lsp.fs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ type OptionallyVersionedTextDocumentPositionParams =
member this.TextDocument with get() = { Uri = this.TextDocument.Uri }
member this.Position with get() = this.Position

[<RequireQualifiedAccess>]
type InlayHintKind = Type | Parameter

type LSPInlayHint = {
Text : string
Pos : Types.Position
Kind : InlayHintKind
}

module Result =
let ofCoreResponse (r: CoreResponse<'a>) =
match r with
Expand Down Expand Up @@ -662,7 +671,7 @@ type FSharpLspServer(backgroundServiceEnabled: bool, state: State, lspClient: FS

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

member x.FSharpInlayHints(p: LspHelpers.FSharpInlayHintsRequest) =
let mapHintKind (k: FsAutoComplete.Core.InlayHints.HintKind): InlayHintKind =
match k with
| FsAutoComplete.Core.InlayHints.HintKind.Type -> InlayHintKind.Type
| FsAutoComplete.Core.InlayHints.HintKind.Parameter -> InlayHintKind.Parameter

logger.info (
Log.setMessage "FSharpInlayHints Request: {parms}"
>> Log.addContextDestructured "parms" p
)

let fn = p.TextDocument.GetFilePath() |> Utils.normalizePath
let fcsRange = protocolRangeToRange (UMX.untag fn) p.Range
fn
|> x.fileHandler (fun fn tyRes lines ->
let hints = commands.InlayHints(lines, tyRes, fcsRange)
let lspHints =
hints
|> Array.map (fun h -> {
Text = h.Text
Pos = fcsPosToLsp h.Pos
Kind = mapHintKind h.Kind
})
AsyncLspResult.success lspHints
)

member x.FSharpPipelineHints(p: FSharpPipelineHintRequest) =
logger.info (
Log.setMessage "FSharpPipelineHints Request: {parms}"
Expand Down Expand Up @@ -2705,6 +2740,7 @@ let startCore backgroundServiceEnabled toolsPath workspaceLoaderFactory =
|> Map.add "fsproj/addFileAbove" (requestHandling (fun s p -> s.FsProjAddFileAbove(p)))
|> Map.add "fsproj/addFileBelow" (requestHandling (fun s p -> s.FsProjAddFileBelow(p)))
|> Map.add "fsproj/addFile" (requestHandling (fun s p -> s.FsProjAddFile(p)))
|> Map.add "fsharp/inlayHints" (requestHandling (fun s p -> s.FSharpInlayHints(p)))

let state =
State.Initial toolsPath workspaceLoaderFactory
Expand Down
6 changes: 6 additions & 0 deletions src/FsAutoComplete/LspHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -848,3 +848,9 @@ let encodeSemanticHighlightRanges (rangesAndHighlights: (struct(Ionide.LanguageS
prev <- currentRange
idx <- idx + 5
Some finalArray


type FSharpInlayHintsRequest = {
TextDocument: TextDocumentIdentifier
Range: Range
}
15 changes: 14 additions & 1 deletion test/FsAutoComplete.Tests.Lsp/Helpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ let logger = Expecto.Logging.Log.create "LSPTests"
type Cacher<'t> = System.Reactive.Subjects.ReplaySubject<'t>
type ClientEvents = IObservable<string * obj>

module Range =
let rangeContainsPos (range : Range) (pos : Position) =
range.Start <= pos && pos <= range.End

let record (cacher: Cacher<_>) =
fun name payload ->
cacher.OnNext (name, payload);
Expand Down Expand Up @@ -482,8 +486,17 @@ let waitForTestDetected (fileName: string) (events: ClientEvents): Async<TestDet
testNotificationFileName = fileName)
|> Async.AwaitObservable


let waitForEditsForFile file =
workspaceEdits
>> editsFor file
>> Async.AwaitObservable

let trySerialize (t: string): 't option =
try
JsonSerializer.readJson t |> Some
with _ -> None

let (|As|_|) (m: PlainNotification): 't option =
match trySerialize m.Content with
| Some(r: FsAutoComplete.CommandResponse.ResponseMsg<'t>) -> Some r.Data
| None -> None
11 changes: 0 additions & 11 deletions test/FsAutoComplete.Tests.Lsp/InfoPanelTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,9 @@ open Expecto
open System.IO
open Ionide.LanguageServerProtocol.Types
open FsAutoComplete
open FsAutoComplete.LspHelpers
open Helpers
open FsToolkit.ErrorHandling

let trySerialize (t: string): 't option =
try
JsonSerializer.readJson t |> Some
with _ -> None

let (|As|_|) (m: PlainNotification): 't option =
match trySerialize m.Content with
| Some(r: FsAutoComplete.CommandResponse.ResponseMsg<'t>) -> Some r.Data
| None -> None

let docFormattingTest state =
let server =
async {
Expand Down
Loading