From c3527477fb4190583b4fcbdb6c4ada7e9751bf5e Mon Sep 17 00:00:00 2001 From: Adam Tao Date: Mon, 27 Feb 2023 21:07:20 +0800 Subject: [PATCH 1/6] feat(Server.fs): Add the framework of inlayHint Signed-off-by: Adam Tao --- src/CSharpLanguageServer/Server.fs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/CSharpLanguageServer/Server.fs b/src/CSharpLanguageServer/Server.fs index 5c43fcfe..41d70769 100644 --- a/src/CSharpLanguageServer/Server.fs +++ b/src/CSharpLanguageServer/Server.fs @@ -1120,6 +1120,29 @@ let setupServerHandlers options (lspClient: LspClient) = let handleSemanticTokensRange (scope: ServerRequestScope) (semParams: Types.SemanticTokensRangeParams): AsyncLspResult = getSemanticTokensRange scope semParams.TextDocument.Uri (Some semParams.Range) + let toInlayHint (semanticModel: SemanticModel) (lines: TextLineCollection) (node: SyntaxNode): InlayHint option = + None + + let handleTextDocumentInlayHint (scope: ServerRequestScope) (inlayHintParams: InlayHintParams): AsyncLspResult = async { + match scope.GetUserDocumentForUri inlayHintParams.TextDocument.Uri with + | None -> return None |> success + | Some doc -> + let! semanticModel = doc.GetSemanticModelAsync() |> Async.AwaitTask + let! root = doc.GetSyntaxRootAsync() |> Async.AwaitTask + let! sourceText = doc.GetTextAsync() |> Async.AwaitTask + let textSpan = + inlayHintParams.Range + |> roslynLinePositionSpanForLspRange + |> sourceText.Lines.GetTextSpan + + let inlayHints = + root.DescendantNodes(textSpan, fun node -> node.Span.IntersectsWith(textSpan)) + |> Seq.map (toInlayHint semanticModel sourceText.Lines) + |> Seq.filter Option.isSome + |> Seq.map Option.get + return inlayHints |> Seq.toArray |> Some |> success + } + let handleWorkspaceSymbol (scope: ServerRequestScope) (symbolParams: Types.WorkspaceSymbolParams): AsyncLspResult = async { let! symbols = findSymbolsInSolution scope.Solution symbolParams.Query (Some 20) return symbols |> Array.ofSeq |> Some |> success @@ -1306,6 +1329,7 @@ let setupServerHandlers options (lspClient: LspClient) = ("textDocument/signatureHelp" , handleTextDocumentSignatureHelp) |> requestHandlingWithReadOnlyScope ("textDocument/semanticTokens/full", handleSemanticTokensFull) |> requestHandlingWithReadOnlyScope ("textDocument/semanticTokens/range", handleSemanticTokensRange) |> requestHandlingWithReadOnlyScope + ("textDocument/inlayHint" , handleTextDocumentInlayHint) |> requestHandlingWithReadOnlyScope ("workspace/symbol" , handleWorkspaceSymbol) |> requestHandlingWithReadOnlyScope ("workspace/didChangeWatchedFiles", handleWorkspaceDidChangeWatchedFiles) |> requestHandlingWithReadWriteScope ("csharp/metadata" , handleCSharpMetadata) |> requestHandlingWithReadOnlyScope From 1f3c8572ad2f85e29c676f6ceb01fe04977a88ef Mon Sep 17 00:00:00 2001 From: Adam Tao Date: Mon, 27 Feb 2023 21:54:34 +0800 Subject: [PATCH 2/6] feat(Server.fs): Support type inlay hint Add type inlay hint to the end of identifiers, just like what clangd does. This commit is almost a rewrite of https://github.com/dotnet/roslyn/blob/main/src/Features/CSharp/Portable/InlineHints/CSharpInlineTypeHintsService.cs. Because Roslyn doesn't exposes the related classes/methods (they are `internal`), we can't call them directly. Therefore, I try to rewrite them using F# in this commit. Signed-off-by: Adam Tao --- src/CSharpLanguageServer/Server.fs | 67 +++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/src/CSharpLanguageServer/Server.fs b/src/CSharpLanguageServer/Server.fs index 41d70769..9f2b6eef 100644 --- a/src/CSharpLanguageServer/Server.fs +++ b/src/CSharpLanguageServer/Server.fs @@ -11,6 +11,7 @@ open Microsoft.CodeAnalysis.FindSymbols open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.Completion open Microsoft.CodeAnalysis.Rename +open Microsoft.CodeAnalysis.CSharp open Microsoft.CodeAnalysis.CSharp.Syntax open Microsoft.CodeAnalysis.CodeFixes open Microsoft.CodeAnalysis.Classification @@ -1121,7 +1122,71 @@ let setupServerHandlers options (lspClient: LspClient) = getSemanticTokensRange scope semParams.TextDocument.Uri (Some semParams.Range) let toInlayHint (semanticModel: SemanticModel) (lines: TextLineCollection) (node: SyntaxNode): InlayHint option = - None + let validateType (ty: ITypeSymbol) = + if isNull ty || ty :? IErrorTypeSymbol || ty.Name = "var" then + None + else + Some ty + let toTypeInlayHint (pos: int) (ty: ITypeSymbol): InlayHint = + { Position = pos |> lines.GetLinePosition |> lspPositionForRoslynLinePosition + Label = InlayHintLabel.String (": " + ty.Name) + Kind = Some InlayHintKind.Type + TextEdits = None + Tooltip = None + PaddingLeft = Some false + PaddingRight = Some false + Data = None + } + + // It's a rewrite of https://github.com/dotnet/roslyn/blob/main/src/Features/CSharp/Portable/InlineHints/CSharpInlineTypeHintsService.cs. + // If Roslyn exposes the classes, then we can just do a type convert. + match node with + | :? VariableDeclarationSyntax as var when + var.Type.IsVar && var.Variables.Count = 1 && not var.Variables[0].Identifier.IsMissing + -> + semanticModel.GetTypeInfo(var.Type).Type + |> validateType + |> Option.map (toTypeInlayHint var.Variables[0].Identifier.Span.End) + | :? DeclarationExpressionSyntax as dec when + dec.Type.IsVar + -> + semanticModel.GetTypeInfo(dec.Type).Type + |> validateType + |> Option.map (toTypeInlayHint dec.Designation.Span.End) + | :? SingleVariableDesignationSyntax as var + -> + semanticModel.GetDeclaredSymbol(var) + |> fun sym -> + match sym with + | :? ILocalSymbol as local -> Some local + | _ -> None + |> Option.map (fun local -> local.Type) + |> Option.bind validateType + |> Option.map (toTypeInlayHint var.Identifier.Span.End) + | :? ForEachStatementSyntax as forEach when + forEach.Type.IsVar + -> + semanticModel.GetForEachStatementInfo(forEach).ElementType + |> validateType + |> Option.map (toTypeInlayHint forEach.Identifier.Span.End) + | :? ParameterSyntax as parameterNode when + isNull parameterNode.Type + -> + let parameter = semanticModel.GetDeclaredSymbol(parameterNode) + parameter + |> fun parameter -> + match parameter.ContainingSymbol with + | :? IMethodSymbol as methSym when methSym.MethodKind = MethodKind.AnonymousFunction -> Some parameter + | _ -> None + |> Option.map (fun parameter -> parameter.Type) + |> Option.bind validateType + |> Option.map (toTypeInlayHint parameterNode.Identifier.Span.End) + | :? ImplicitObjectCreationExpressionSyntax as implicitNew + -> + semanticModel.GetTypeInfo(implicitNew).Type + |> validateType + |> Option.map (toTypeInlayHint implicitNew.NewKeyword.Span.End) + | _ -> None let handleTextDocumentInlayHint (scope: ServerRequestScope) (inlayHintParams: InlayHintParams): AsyncLspResult = async { match scope.GetUserDocumentForUri inlayHintParams.TextDocument.Uri with From 8ef6110f47681b871e6789becac61431c577c710 Mon Sep 17 00:00:00 2001 From: Adam Tao Date: Mon, 27 Feb 2023 23:49:29 +0800 Subject: [PATCH 3/6] feat(RoslynHelpers.fs): Add helpers for parameter inlay hint Signed-off-by: Adam Tao --- src/CSharpLanguageServer/RoslynHelpers.fs | 64 +++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/CSharpLanguageServer/RoslynHelpers.fs b/src/CSharpLanguageServer/RoslynHelpers.fs index 771e1b37..a1ae3704 100644 --- a/src/CSharpLanguageServer/RoslynHelpers.fs +++ b/src/CSharpLanguageServer/RoslynHelpers.fs @@ -1186,3 +1186,67 @@ let makeDocumentFromMetadata let mdDocument = SourceText.From(text) |> mdDocumentEmpty.WithText (mdDocument, text) + +let getBestOrAllSymbols (info: SymbolInfo) = + let best = if isNull info.Symbol then None else Some ([| info.Symbol |]) + let all = if info.CandidateSymbols.IsEmpty then None else Some (info.CandidateSymbols |> Array.ofSeq) + best |> Option.orElse all |> Option.defaultValue Array.empty + +// rewrite of https://github.com/dotnet/roslyn/blob/main/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/ArgumentSyntaxExtensions.cs +let getParameterForArgumentSyntax (semanticModel: SemanticModel) (argument: ArgumentSyntax) : IParameterSymbol option = + match argument.Parent with + | :? BaseArgumentListSyntax as argumentList when not (isNull argumentList.Parent) -> + let symbols = semanticModel.GetSymbolInfo(argumentList.Parent) |> getBestOrAllSymbols + match symbols with + | [| symbol |] -> + let parameters = + match symbol with + | :? IMethodSymbol as m -> m.Parameters + | :? IPropertySymbol as nt -> nt.Parameters + | _ -> ImmutableArray.Empty + let namedParameter = + if isNull argument.NameColon || argument.NameColon.IsMissing then + None + else + parameters |> Seq.tryFind (fun p -> p.Name = argument.NameColon.Name.Identifier.ValueText) + let positionalParameter = + match argumentList.Arguments.IndexOf(argument) with + | index when 0 <= index && index < parameters.Length -> + let parameter = parameters[index] + if argument.RefOrOutKeyword.Kind() = SyntaxKind.OutKeyword && parameter.RefKind <> RefKind.Out || + argument.RefOrOutKeyword.Kind() = SyntaxKind.RefKeyword && parameter.RefKind <> RefKind.Ref then + None + else + Some parameter + | _ -> None + namedParameter |> Option.orElse positionalParameter + | _ -> None + | _ -> None + +// rewrite of https://github.com/dotnet/roslyn/blob/main/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/AttributeArgumentSyntaxExtensions.cs +let getParameterForAttributeArgumentSyntax (semanticModel: SemanticModel) (argument: AttributeArgumentSyntax) : IParameterSymbol option = + match argument.Parent with + | :? AttributeArgumentListSyntax as argumentList when not (isNull argument.NameEquals) -> + match argumentList.Parent with + | :? AttributeSyntax as invocable -> + let symbols = semanticModel.GetSymbolInfo(invocable) |> getBestOrAllSymbols + match symbols with + | [| symbol |] -> + let parameters = + match symbol with + | :? IMethodSymbol as m -> m.Parameters + | :? IPropertySymbol as nt -> nt.Parameters + | _ -> ImmutableArray.Empty + let namedParameter = + if isNull argument.NameColon || argument.NameColon.IsMissing then + None + else + parameters |> Seq.tryFind (fun p -> p.Name = argument.NameColon.Name.Identifier.ValueText) + let positionalParameter = + match argumentList.Arguments.IndexOf(argument) with + | index when 0 <= index && index < parameters.Length -> Some parameters[index] + | _ -> None + namedParameter |> Option.orElse positionalParameter + | _ -> None + | _ -> None + | _ -> None From f52b7e66103dccf6475444a68a6b9bd386394eca Mon Sep 17 00:00:00 2001 From: Adam Tao Date: Tue, 28 Feb 2023 00:15:06 +0800 Subject: [PATCH 4/6] feat(Server.fs): Support parameter inlay hint Add parameter inlay hints before afguments, just like what clangd does. This commit is almost a rewrite of https://github.com/dotnet/roslyn/blob/main/src/Features/CSharp/Portable/InlineHints/CSharpInlineParameterNameHintsService.cs. Signed-off-by: Adam Tao --- src/CSharpLanguageServer/Server.fs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/CSharpLanguageServer/Server.fs b/src/CSharpLanguageServer/Server.fs index 9f2b6eef..28533c83 100644 --- a/src/CSharpLanguageServer/Server.fs +++ b/src/CSharpLanguageServer/Server.fs @@ -1138,7 +1138,19 @@ let setupServerHandlers options (lspClient: LspClient) = Data = None } - // It's a rewrite of https://github.com/dotnet/roslyn/blob/main/src/Features/CSharp/Portable/InlineHints/CSharpInlineTypeHintsService.cs. + let toParameterInlayHint (pos: int) (par: IParameterSymbol): InlayHint = + { Position = pos |> lines.GetLinePosition |> lspPositionForRoslynLinePosition + Label = InlayHintLabel.String (par.Name + ":") + Kind = Some InlayHintKind.Parameter + TextEdits = None + Tooltip = None + PaddingLeft = Some false + PaddingRight = Some true + Data = None + } + + // It's a rewrite of https://github.com/dotnet/roslyn/blob/main/src/Features/CSharp/Portable/InlineHints/CSharpInlineTypeHintsService.cs & + // https://github.com/dotnet/roslyn/blob/main/src/Features/CSharp/Portable/InlineHints/CSharpInlineParameterNameHintsService.cs. // If Roslyn exposes the classes, then we can just do a type convert. match node with | :? VariableDeclarationSyntax as var when @@ -1186,6 +1198,20 @@ let setupServerHandlers options (lspClient: LspClient) = semanticModel.GetTypeInfo(implicitNew).Type |> validateType |> Option.map (toTypeInlayHint implicitNew.NewKeyword.Span.End) + + | :? ArgumentSyntax as argument when + isNull argument.NameColon + -> + argument + |> getParameterForArgumentSyntax semanticModel + |> Option.map (toParameterInlayHint argument.Span.Start) // TODO: filter out indexer parameters + | :? AttributeArgumentSyntax as argument when + isNull argument.NameEquals && isNull argument.NameColon + -> + argument + |> getParameterForAttributeArgumentSyntax semanticModel + |> Option.map (toParameterInlayHint argument.Span.Start) + | _ -> None let handleTextDocumentInlayHint (scope: ServerRequestScope) (inlayHintParams: InlayHintParams): AsyncLspResult = async { From 015dbfe711d29d1721520cf4931aa5c640341843 Mon Sep 17 00:00:00 2001 From: Adam Tao Date: Tue, 28 Feb 2023 00:15:43 +0800 Subject: [PATCH 5/6] feat(Server.fs): Add inlay hint to server capability Signed-off-by: Adam Tao --- src/CSharpLanguageServer/Server.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CSharpLanguageServer/Server.fs b/src/CSharpLanguageServer/Server.fs index 28533c83..d5193408 100644 --- a/src/CSharpLanguageServer/Server.fs +++ b/src/CSharpLanguageServer/Server.fs @@ -279,6 +279,7 @@ let setupServerHandlers options (lspClient: LspClient) = Range = Some true Full = true |> First |> Some } + InlayHintProvider = Some { ResolveProvider = Some false } } } From df1c2478c4700a796db4c0a0b974ebb227432cf0 Mon Sep 17 00:00:00 2001 From: Adam Tao Date: Tue, 28 Feb 2023 20:12:58 +0800 Subject: [PATCH 6/6] feat(Server.fs): Suppress some kinds of parameter inlay hint 1. Suppress parameter hint for indexer. 2. Suppress parameter hint if argument matches parameter name. Signed-off-by: Adam Tao --- src/CSharpLanguageServer/Server.fs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/CSharpLanguageServer/Server.fs b/src/CSharpLanguageServer/Server.fs index d5193408..35dbc26d 100644 --- a/src/CSharpLanguageServer/Server.fs +++ b/src/CSharpLanguageServer/Server.fs @@ -1139,6 +1139,13 @@ let setupServerHandlers options (lspClient: LspClient) = Data = None } + let validateParameter (arg: SyntaxNode) (par: IParameterSymbol) = + match arg.Parent with + // Don't show hint for indexer + | :? BracketedArgumentListSyntax -> None + // Don't show hint if argument matches parameter name + | _ when String.Equals(arg.GetText().ToString(), par.Name, StringComparison.CurrentCultureIgnoreCase) -> None + | _ -> Some par let toParameterInlayHint (pos: int) (par: IParameterSymbol): InlayHint = { Position = pos |> lines.GetLinePosition |> lspPositionForRoslynLinePosition Label = InlayHintLabel.String (par.Name + ":") @@ -1205,12 +1212,14 @@ let setupServerHandlers options (lspClient: LspClient) = -> argument |> getParameterForArgumentSyntax semanticModel - |> Option.map (toParameterInlayHint argument.Span.Start) // TODO: filter out indexer parameters + |> Option.bind (validateParameter node) + |> Option.map (toParameterInlayHint argument.Span.Start) | :? AttributeArgumentSyntax as argument when isNull argument.NameEquals && isNull argument.NameColon -> argument |> getParameterForAttributeArgumentSyntax semanticModel + |> Option.bind (validateParameter node) |> Option.map (toParameterInlayHint argument.Span.Start) | _ -> None