diff --git a/src/CSharpLanguageServer/CSharpLanguageServer.fsproj b/src/CSharpLanguageServer/CSharpLanguageServer.fsproj index 17671f98..8e2c76d7 100644 --- a/src/CSharpLanguageServer/CSharpLanguageServer.fsproj +++ b/src/CSharpLanguageServer/CSharpLanguageServer.fsproj @@ -23,11 +23,13 @@ - - - - + + + + + + diff --git a/src/CSharpLanguageServer/Diagnostics.fs b/src/CSharpLanguageServer/Diagnostics.fs index 56fa07e1..4eaea02c 100644 --- a/src/CSharpLanguageServer/Diagnostics.fs +++ b/src/CSharpLanguageServer/Diagnostics.fs @@ -8,7 +8,7 @@ open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.Types open CSharpLanguageServer.Logging -open CSharpLanguageServer.RoslynHelpers +open CSharpLanguageServer.Roslyn.Solution module Diagnostics = let private logger = Logging.getLoggerByName "Diagnostics" @@ -58,13 +58,13 @@ module Diagnostics = let diagnose (settings: ServerSettings) : Async = async { logger.LogDebug("diagnose: settings={settings}", settings) - initializeMSBuild logger + initializeMSBuild () logger.LogDebug("diagnose: loading solution..") let lspClient = new LspClientStub() let cwd = string (Directory.GetCurrentDirectory()) - let! _sln = loadSolutionOnSolutionPathOrDir lspClient logger None cwd + let! _sln = solutionLoadSolutionWithPathOrOnCwd lspClient None cwd logger.LogDebug("diagnose: done") diff --git a/src/CSharpLanguageServer/DocumentationUtil.fs b/src/CSharpLanguageServer/DocumentationUtil.fs index 38884c64..8e2deb71 100644 --- a/src/CSharpLanguageServer/DocumentationUtil.fs +++ b/src/CSharpLanguageServer/DocumentationUtil.fs @@ -5,7 +5,7 @@ open System.Xml.Linq open Microsoft.CodeAnalysis -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions module DocumentationUtil = type TripleSlashComment = diff --git a/src/CSharpLanguageServer/FormatUtil.fs b/src/CSharpLanguageServer/FormatUtil.fs deleted file mode 100644 index f802b088..00000000 --- a/src/CSharpLanguageServer/FormatUtil.fs +++ /dev/null @@ -1,104 +0,0 @@ -namespace CSharpLanguageServer - -open Microsoft.CodeAnalysis -open Microsoft.CodeAnalysis.CSharp.Formatting -open Microsoft.CodeAnalysis.Options -open Microsoft.CodeAnalysis.Text -open Microsoft.CodeAnalysis.Formatting -open Ionide.LanguageServerProtocol.Types - -open CSharpLanguageServer.Types - -module internal FormatUtil = - let processChange (oldText: SourceText) (change: TextChange) : TextEdit = - let mapToTextEdit (linePosition: LinePositionSpan, newText: string) : TextEdit = - { NewText = newText - Range = - { Start = - { Line = uint32 linePosition.Start.Line - Character = uint32 linePosition.Start.Character } - End = - { Line = uint32 linePosition.End.Line - Character = uint32 linePosition.End.Character } } } - - let defaultTextEdit (oldText: SourceText, change: TextChange) : TextEdit = - let linePosition = oldText.Lines.GetLinePositionSpan change.Span - mapToTextEdit (linePosition, change.NewText) - - let padLeft (span: TextSpan) : TextSpan = - TextSpan.FromBounds(span.Start - 1, span.End) - - let padRight (span: TextSpan) : TextSpan = - TextSpan.FromBounds(span.Start, span.End + 1) - - let rec checkSpanLineEndings (newText: string, oldText: SourceText, span: TextSpan, prefix: string) : TextEdit = - if span.Start > 0 && newText[0].Equals '\n' && oldText[span.Start - 1].Equals '\r' then - checkSpanLineEndings (newText, oldText, padLeft span, "\r") |> ignore - - if - span.End < oldText.Length - 1 - && newText[newText.Length - 1].Equals '\r' - && oldText[span.End].Equals '\n' - then - let linePosition = oldText.Lines.GetLinePositionSpan(padRight span) - mapToTextEdit (linePosition, prefix + newText.ToString() + "\n") - else - let linePosition = oldText.Lines.GetLinePositionSpan span - mapToTextEdit (linePosition, newText.ToString()) - - let newText = change.NewText - - if newText.Length > 0 then - checkSpanLineEndings (newText, oldText, change.Span, "") - else - defaultTextEdit (oldText, change) - - let convert (oldText: SourceText) (changes: TextChange[]) : TextEdit[] = - //why doesnt it pick up that TextSpan implements IComparable? - //one of life's many mysteries - let comparer (lhs: TextChange) (rhs: TextChange) : int = lhs.Span.CompareTo rhs.Span - - changes - |> Seq.sortWith comparer - |> Seq.map (fun x -> processChange oldText x) - |> Seq.toArray - - let getChanges (doc: Document) (oldDoc: Document) : Async = async { - let! ct = Async.CancellationToken - let! changes = doc.GetTextChangesAsync(oldDoc, ct) |> Async.AwaitTask - let! oldText = oldDoc.GetTextAsync ct |> Async.AwaitTask - return convert oldText (changes |> Seq.toArray) - } - - let getFormattingOptions - (settings: ServerSettings) - (doc: Document) - (formattingOptions: FormattingOptions) - : Async = - async { - let! docOptions = doc.GetOptionsAsync() |> Async.AwaitTask - - return - match settings.ApplyFormattingOptions with - | false -> docOptions - | true -> - docOptions - |> _.WithChangedOption( - FormattingOptions.IndentationSize, - LanguageNames.CSharp, - int formattingOptions.TabSize - ) - |> _.WithChangedOption( - FormattingOptions.UseTabs, - LanguageNames.CSharp, - not formattingOptions.InsertSpaces - ) - |> match formattingOptions.InsertFinalNewline with - | Some insertFinalNewline -> - _.WithChangedOption(CSharpFormattingOptions.NewLineForFinally, insertFinalNewline) - | None -> id - |> match formattingOptions.TrimFinalNewlines with - | Some trimFinalNewlines -> - _.WithChangedOption(CSharpFormattingOptions.NewLineForFinally, not trimFinalNewlines) - | None -> id - } diff --git a/src/CSharpLanguageServer/Handlers/CallHierarchy.fs b/src/CSharpLanguageServer/Handlers/CallHierarchy.fs index ece9389c..b833e544 100644 --- a/src/CSharpLanguageServer/Handlers/CallHierarchy.fs +++ b/src/CSharpLanguageServer/Handlers/CallHierarchy.fs @@ -6,7 +6,7 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions [] module CallHierarchy = diff --git a/src/CSharpLanguageServer/Handlers/CodeAction.fs b/src/CSharpLanguageServer/Handlers/CodeAction.fs index e4129083..407fc3fb 100644 --- a/src/CSharpLanguageServer/Handlers/CodeAction.fs +++ b/src/CSharpLanguageServer/Handlers/CodeAction.fs @@ -18,7 +18,7 @@ open Ionide.LanguageServerProtocol.JsonRpc open Microsoft.Extensions.Logging open CSharpLanguageServer.Logging -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.State open CSharpLanguageServer.Util diff --git a/src/CSharpLanguageServer/Handlers/CodeLens.fs b/src/CSharpLanguageServer/Handlers/CodeLens.fs index 11955441..6ec0fb1c 100644 --- a/src/CSharpLanguageServer/Handlers/CodeLens.fs +++ b/src/CSharpLanguageServer/Handlers/CodeLens.fs @@ -9,7 +9,7 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions type private DocumentSymbolCollectorForCodeLens(semanticModel: SemanticModel) = inherit CSharpSyntaxWalker(SyntaxWalkerDepth.Token) diff --git a/src/CSharpLanguageServer/Handlers/Completion.fs b/src/CSharpLanguageServer/Handlers/Completion.fs index 6d16fb7e..f26e697a 100644 --- a/src/CSharpLanguageServer/Handlers/Completion.fs +++ b/src/CSharpLanguageServer/Handlers/Completion.fs @@ -10,7 +10,7 @@ open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State open CSharpLanguageServer.Util -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Logging [] @@ -202,7 +202,19 @@ module Completion = |> _.WithBool("ShowItemsFromUnimportedNamespaces", false) |> _.WithBool("ShowNameSuggestions", false) - let completionTrigger = CompletionContext.toCompletionTrigger p.Context + let completionTrigger = + p.Context + |> Option.bind (fun ctx -> + match ctx.TriggerKind with + | CompletionTriggerKind.Invoked + | CompletionTriggerKind.TriggerForIncompleteCompletions -> + Some Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke + | CompletionTriggerKind.TriggerCharacter -> + ctx.TriggerCharacter + |> Option.map Seq.head + |> Option.map Microsoft.CodeAnalysis.Completion.CompletionTrigger.CreateInsertionTrigger + | _ -> None) + |> Option.defaultValue Microsoft.CodeAnalysis.Completion.CompletionTrigger.Invoke let shouldTriggerCompletion = p.Context diff --git a/src/CSharpLanguageServer/Handlers/Diagnostic.fs b/src/CSharpLanguageServer/Handlers/Diagnostic.fs index 57fda17f..202a3625 100644 --- a/src/CSharpLanguageServer/Handlers/Diagnostic.fs +++ b/src/CSharpLanguageServer/Handlers/Diagnostic.fs @@ -5,7 +5,7 @@ open Ionide.LanguageServerProtocol.Server open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.State open CSharpLanguageServer.Types diff --git a/src/CSharpLanguageServer/Handlers/DocumentFormatting.fs b/src/CSharpLanguageServer/Handlers/DocumentFormatting.fs index 497625da..46667f4a 100644 --- a/src/CSharpLanguageServer/Handlers/DocumentFormatting.fs +++ b/src/CSharpLanguageServer/Handlers/DocumentFormatting.fs @@ -4,21 +4,30 @@ open Microsoft.CodeAnalysis.Formatting open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc -open CSharpLanguageServer open CSharpLanguageServer.State +open CSharpLanguageServer.Util +open CSharpLanguageServer.Roslyn.Document [] + module DocumentFormatting = - let provider (clientCapabilities: ClientCapabilities) : U2 option = - Some(U2.C1 true) + let provider (_cc: ClientCapabilities) : U2 option = Some(U2.C1 true) - let handle (context: ServerRequestContext) (p: DocumentFormattingParams) : AsyncLspResult = async { - match context.GetUserDocument p.TextDocument.Uri with - | None -> return None |> LspResult.success - | Some doc -> - let! ct = Async.CancellationToken - let! options = FormatUtil.getFormattingOptions context.State.Settings doc p.Options - let! newDoc = Formatter.FormatAsync(doc, options, cancellationToken = ct) |> Async.AwaitTask - let! textEdits = FormatUtil.getChanges newDoc doc - return textEdits |> Some |> LspResult.success + let formatDocument lspFormattingOptions doc : Async = async { + let! ct = Async.CancellationToken + let! options = getDocumentFormattingOptionSet doc lspFormattingOptions + let! newDoc = Formatter.FormatAsync(doc, options, cancellationToken = ct) |> Async.AwaitTask + let! textEdits = getDocumentDiffAsLspTextEdits newDoc doc + return textEdits |> Some } + + let handle (context: ServerRequestContext) (p: DocumentFormattingParams) : AsyncLspResult = + let formatDocument = + p.Options + |> context.State.Settings.GetEffectiveFormattingOptions + |> formatDocument + + context.GetUserDocument p.TextDocument.Uri + |> async.Return + |> Async.bindOption formatDocument + |> Async.map LspResult.success diff --git a/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs b/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs index 56a01698..02a1eff6 100644 --- a/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs +++ b/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs @@ -8,7 +8,7 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions [] module DocumentHighlight = diff --git a/src/CSharpLanguageServer/Handlers/DocumentOnTypeFormatting.fs b/src/CSharpLanguageServer/Handlers/DocumentOnTypeFormatting.fs index 98894a04..c5b91b8e 100644 --- a/src/CSharpLanguageServer/Handlers/DocumentOnTypeFormatting.fs +++ b/src/CSharpLanguageServer/Handlers/DocumentOnTypeFormatting.fs @@ -7,9 +7,9 @@ open Microsoft.CodeAnalysis.Formatting open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc -open CSharpLanguageServer open CSharpLanguageServer.State -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions +open CSharpLanguageServer.Roslyn.Document [] module DocumentOnTypeFormatting = @@ -42,10 +42,16 @@ module DocumentOnTypeFormatting = | _ -> None let handle (context: ServerRequestContext) (p: DocumentOnTypeFormattingParams) : AsyncLspResult = async { + let lspFormattingOptions = + if context.State.Settings.ApplyFormattingOptions then + Some p.Options + else + None + match context.GetUserDocument p.TextDocument.Uri with | None -> return None |> LspResult.success | Some doc -> - let! options = FormatUtil.getFormattingOptions context.State.Settings doc p.Options + let! options = getDocumentFormattingOptionSet doc lspFormattingOptions let! ct = Async.CancellationToken let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask let pos = Position.toRoslynPosition sourceText.Lines p.Position @@ -69,7 +75,7 @@ module DocumentOnTypeFormatting = ) |> Async.AwaitTask - let! textEdits = FormatUtil.getChanges newDoc doc + let! textEdits = getDocumentDiffAsLspTextEdits newDoc doc return textEdits |> Some |> LspResult.success | _ -> return None |> LspResult.success } diff --git a/src/CSharpLanguageServer/Handlers/DocumentRangeFormatting.fs b/src/CSharpLanguageServer/Handlers/DocumentRangeFormatting.fs index 5927d1b7..a42be2c0 100644 --- a/src/CSharpLanguageServer/Handlers/DocumentRangeFormatting.fs +++ b/src/CSharpLanguageServer/Handlers/DocumentRangeFormatting.fs @@ -5,20 +5,27 @@ open Microsoft.CodeAnalysis.Text open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc -open CSharpLanguageServer open CSharpLanguageServer.State -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions +open CSharpLanguageServer.Roslyn.Document [] module DocumentRangeFormatting = - let provider (_: ClientCapabilities) : U2 option = Some(U2.C1 true) + let provider (_cc: ClientCapabilities) : U2 option = Some(U2.C1 true) let handle (context: ServerRequestContext) (p: DocumentRangeFormattingParams) : AsyncLspResult = async { + let lspFormattingOptions = + if context.State.Settings.ApplyFormattingOptions then + Some p.Options + else + None + match context.GetUserDocument p.TextDocument.Uri with | None -> return None |> LspResult.success | Some doc -> let! ct = Async.CancellationToken - let! options = FormatUtil.getFormattingOptions context.State.Settings doc p.Options + + let! options = getDocumentFormattingOptionSet doc lspFormattingOptions let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask let startPos = Position.toRoslynPosition sourceText.Lines p.Range.Start let endPos = Position.toRoslynPosition sourceText.Lines p.Range.End @@ -29,6 +36,6 @@ module DocumentRangeFormatting = Formatter.FormatAsync(doc, TextSpan.FromBounds(tokenStart, endPos), options, cancellationToken = ct) |> Async.AwaitTask - let! textEdits = FormatUtil.getChanges newDoc doc + let! textEdits = getDocumentDiffAsLspTextEdits newDoc doc return textEdits |> Some |> LspResult.success } diff --git a/src/CSharpLanguageServer/Handlers/DocumentSymbol.fs b/src/CSharpLanguageServer/Handlers/DocumentSymbol.fs index f32c286f..d1f796b5 100644 --- a/src/CSharpLanguageServer/Handlers/DocumentSymbol.fs +++ b/src/CSharpLanguageServer/Handlers/DocumentSymbol.fs @@ -10,7 +10,7 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Util [] diff --git a/src/CSharpLanguageServer/Handlers/Hover.fs b/src/CSharpLanguageServer/Handlers/Hover.fs index 6490baa4..35d59b61 100644 --- a/src/CSharpLanguageServer/Handlers/Hover.fs +++ b/src/CSharpLanguageServer/Handlers/Hover.fs @@ -5,24 +5,25 @@ open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer open CSharpLanguageServer.State +open CSharpLanguageServer.Util [] module Hover = - let provider (_: ClientCapabilities) : U2 option = Some(U2.C1 true) + let provider (_cc: ClientCapabilities) : U2 option = Some(U2.C1 true) - let handle (context: ServerRequestContext) (p: HoverParams) : AsyncLspResult = async { - match! context.FindSymbol' p.TextDocument.Uri p.Position with - | None -> return None |> LspResult.success - | Some(symbol, _, _) -> - let content = DocumentationUtil.markdownDocForSymbolWithSignature symbol + let makeHoverForSymbol symbol = + let content = DocumentationUtil.markdownDocForSymbolWithSignature symbol - let hover = - { Contents = - { Kind = MarkupKind.Markdown - Value = content } - |> U3.C1 - // TODO: Support range - Range = None } + let hover = + { Contents = + { Kind = MarkupKind.Markdown + Value = content } + |> U3.C1 + Range = None } // TODO: Support range - return hover |> Some |> LspResult.success - } + hover |> Some + + let handle (context: ServerRequestContext) (p: HoverParams) : AsyncLspResult = + context.FindSymbol p.TextDocument.Uri p.Position + |> Async.bindOption (makeHoverForSymbol >> async.Return) + |> Async.map LspResult.success diff --git a/src/CSharpLanguageServer/Handlers/Implementation.fs b/src/CSharpLanguageServer/Handlers/Implementation.fs index 5845614a..ba7981f9 100644 --- a/src/CSharpLanguageServer/Handlers/Implementation.fs +++ b/src/CSharpLanguageServer/Handlers/Implementation.fs @@ -11,22 +11,17 @@ module Implementation = let provider (_: ClientCapabilities) : U3 option = Some(U3.C1 true) + let findImplementationsOfSymbol (context: ServerRequestContext) sym = async { + let! impls = context.FindImplementations sym + let! locations = impls |> Seq.map (flip context.ResolveSymbolLocations None) |> Async.Parallel + + return locations |> Array.collect List.toArray |> Declaration.C2 |> U2.C1 |> Some + } + let handle (context: ServerRequestContext) (p: ImplementationParams) : Async option>> = - async { - match! context.FindSymbol p.TextDocument.Uri p.Position with - | None -> return None |> LspResult.success - | Some symbol -> - let! impls = context.FindImplementations symbol - let! locations = impls |> Seq.map (flip context.ResolveSymbolLocations None) |> Async.Parallel - - return - locations - |> Array.collect List.toArray - |> Declaration.C2 - |> U2.C1 - |> Some - |> LspResult.success - } + context.FindSymbol p.TextDocument.Uri p.Position + |> Async.bindOption (findImplementationsOfSymbol context) + |> Async.map LspResult.success diff --git a/src/CSharpLanguageServer/Handlers/Initialization.fs b/src/CSharpLanguageServer/Handlers/Initialization.fs index 45109d3c..621fa55d 100644 --- a/src/CSharpLanguageServer/Handlers/Initialization.fs +++ b/src/CSharpLanguageServer/Handlers/Initialization.fs @@ -14,7 +14,8 @@ open CSharpLanguageServer.State open CSharpLanguageServer.State.ServerState open CSharpLanguageServer.Types open CSharpLanguageServer.Logging -open CSharpLanguageServer.RoslynHelpers +open CSharpLanguageServer.Roslyn.Solution + [] module Initialization = @@ -53,7 +54,7 @@ module Initialization = serverName ) - initializeMSBuild logger + initializeMSBuild () logger.LogDebug("handleInitialize: p.Capabilities={caps}", serialize p.Capabilities) context.Emit(ClientCapabilityChange p.Capabilities) diff --git a/src/CSharpLanguageServer/Handlers/InlayHint.fs b/src/CSharpLanguageServer/Handlers/InlayHint.fs index fa6a5550..356ece62 100644 --- a/src/CSharpLanguageServer/Handlers/InlayHint.fs +++ b/src/CSharpLanguageServer/Handlers/InlayHint.fs @@ -11,7 +11,7 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Util [] diff --git a/src/CSharpLanguageServer/Handlers/References.fs b/src/CSharpLanguageServer/Handlers/References.fs index 1edc57a6..49d1d50d 100644 --- a/src/CSharpLanguageServer/Handlers/References.fs +++ b/src/CSharpLanguageServer/Handlers/References.fs @@ -4,7 +4,7 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Logging [] diff --git a/src/CSharpLanguageServer/Handlers/Rename.fs b/src/CSharpLanguageServer/Handlers/Rename.fs index 927175ae..32b85dfc 100644 --- a/src/CSharpLanguageServer/Handlers/Rename.fs +++ b/src/CSharpLanguageServer/Handlers/Rename.fs @@ -12,7 +12,7 @@ open Microsoft.Extensions.Logging open CSharpLanguageServer.State open CSharpLanguageServer.Logging -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Util [] diff --git a/src/CSharpLanguageServer/Handlers/SemanticTokens.fs b/src/CSharpLanguageServer/Handlers/SemanticTokens.fs index 75b51153..9afc6107 100644 --- a/src/CSharpLanguageServer/Handlers/SemanticTokens.fs +++ b/src/CSharpLanguageServer/Handlers/SemanticTokens.fs @@ -9,7 +9,7 @@ open Microsoft.CodeAnalysis.Text open CSharpLanguageServer.State open CSharpLanguageServer.Util -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions [] module SemanticTokens = diff --git a/src/CSharpLanguageServer/Handlers/SignatureHelp.fs b/src/CSharpLanguageServer/Handlers/SignatureHelp.fs index 1f02964c..6eb32b21 100644 --- a/src/CSharpLanguageServer/Handlers/SignatureHelp.fs +++ b/src/CSharpLanguageServer/Handlers/SignatureHelp.fs @@ -12,7 +12,7 @@ open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer open CSharpLanguageServer.State open CSharpLanguageServer.Util -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Types module SignatureInformation = diff --git a/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs b/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs index 1e7685d2..31498973 100644 --- a/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs +++ b/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs @@ -7,10 +7,11 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.State open CSharpLanguageServer.State.ServerState -open CSharpLanguageServer.RoslynHelpers +open CSharpLanguageServer.Roslyn.Symbol +open CSharpLanguageServer.Roslyn.Solution open CSharpLanguageServer.Logging [] @@ -66,7 +67,7 @@ module TextDocumentSync = match docFilePathMaybe with | Some docFilePath -> async { // ok, this document is not in solution, register a new document - let! newDocMaybe = tryAddDocument logger docFilePath openParams.TextDocument.Text context.Solution + let! newDocMaybe = solutionTryAddDocument docFilePath openParams.TextDocument.Text context.Solution match newDocMaybe with | Some newDoc -> @@ -130,7 +131,7 @@ module TextDocumentSync = | None -> async { let docFilePath = Util.parseFileUri saveParams.TextDocument.Uri - let! newDocMaybe = tryAddDocument logger docFilePath saveParams.Text.Value context.Solution + let! newDocMaybe = solutionTryAddDocument docFilePath saveParams.Text.Value context.Solution match newDocMaybe with | Some newDoc -> diff --git a/src/CSharpLanguageServer/Handlers/TypeHierarchy.fs b/src/CSharpLanguageServer/Handlers/TypeHierarchy.fs index 9a387f76..7fbae774 100644 --- a/src/CSharpLanguageServer/Handlers/TypeHierarchy.fs +++ b/src/CSharpLanguageServer/Handlers/TypeHierarchy.fs @@ -5,7 +5,7 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Util [] diff --git a/src/CSharpLanguageServer/Handlers/Workspace.fs b/src/CSharpLanguageServer/Handlers/Workspace.fs index a18a1aa1..d7a5f9f0 100644 --- a/src/CSharpLanguageServer/Handlers/Workspace.fs +++ b/src/CSharpLanguageServer/Handlers/Workspace.fs @@ -11,7 +11,8 @@ open Microsoft.CodeAnalysis.Text open CSharpLanguageServer open CSharpLanguageServer.State open CSharpLanguageServer.State.ServerState -open CSharpLanguageServer.RoslynHelpers +open CSharpLanguageServer.Roslyn.Symbol +open CSharpLanguageServer.Roslyn.Solution open CSharpLanguageServer.Logging open CSharpLanguageServer.Types @@ -61,7 +62,7 @@ module Workspace = | Some docFilePath -> // ok, this document is not on solution, register a new one let fileText = docFilePath |> File.ReadAllText - let! newDocMaybe = tryAddDocument logger docFilePath fileText context.Solution + let! newDocMaybe = solutionTryAddDocument docFilePath fileText context.Solution match newDocMaybe with | Some newDoc -> context.Emit(SolutionChange newDoc.Project.Solution) diff --git a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs index 9bfd63a2..5defb325 100644 --- a/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs +++ b/src/CSharpLanguageServer/Handlers/WorkspaceSymbol.fs @@ -7,7 +7,7 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc open CSharpLanguageServer.State -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions [] module WorkspaceSymbol = diff --git a/src/CSharpLanguageServer/ProgressReporter.fs b/src/CSharpLanguageServer/Lsp/ProgressReporter.fs similarity index 98% rename from src/CSharpLanguageServer/ProgressReporter.fs rename to src/CSharpLanguageServer/Lsp/ProgressReporter.fs index dffcd54a..d475c15d 100644 --- a/src/CSharpLanguageServer/ProgressReporter.fs +++ b/src/CSharpLanguageServer/Lsp/ProgressReporter.fs @@ -1,4 +1,4 @@ -namespace CSharpLanguageServer +namespace CSharpLanguageServer.Lsp open System open Ionide.LanguageServerProtocol diff --git a/src/CSharpLanguageServer/Conversions.fs b/src/CSharpLanguageServer/Roslyn/Conversions.fs similarity index 93% rename from src/CSharpLanguageServer/Conversions.fs rename to src/CSharpLanguageServer/Roslyn/Conversions.fs index 7078593e..87e113e7 100644 --- a/src/CSharpLanguageServer/Conversions.fs +++ b/src/CSharpLanguageServer/Roslyn/Conversions.fs @@ -1,15 +1,15 @@ -namespace CSharpLanguageServer.Conversions +module CSharpLanguageServer.Roslyn.Conversions open System open System.IO open Microsoft.CodeAnalysis -open Microsoft.CodeAnalysis.Completion open Microsoft.CodeAnalysis.Text open Ionide.LanguageServerProtocol.Types open CSharpLanguageServer.Util + module Uri = // Unescape some necessary char before passing string to Uri. // Can't use Uri.UnescapeDataString here. For example, if uri is "file:///z%3a/src/c%23/ProjDir" ("%3a" is @@ -262,18 +262,3 @@ module Diagnostic = Data = None } (diagnostic, mappedLineSpan.Path |> Uri.fromPath) - - -module CompletionContext = - let toCompletionTrigger (context: CompletionContext option) : CompletionTrigger = - context - |> Option.bind (fun ctx -> - match ctx.TriggerKind with - | CompletionTriggerKind.Invoked - | CompletionTriggerKind.TriggerForIncompleteCompletions -> Some CompletionTrigger.Invoke - | CompletionTriggerKind.TriggerCharacter -> - ctx.TriggerCharacter - |> Option.map Seq.head - |> Option.map CompletionTrigger.CreateInsertionTrigger - | _ -> None) - |> Option.defaultValue CompletionTrigger.Invoke diff --git a/src/CSharpLanguageServer/Roslyn/Document.fs b/src/CSharpLanguageServer/Roslyn/Document.fs new file mode 100644 index 00000000..63ceb2ce --- /dev/null +++ b/src/CSharpLanguageServer/Roslyn/Document.fs @@ -0,0 +1,152 @@ +module CSharpLanguageServer.Roslyn.Document + +open System + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.CSharp.Formatting +open Microsoft.CodeAnalysis.Options +open Microsoft.CodeAnalysis.Text +open Microsoft.CodeAnalysis.Formatting +open Ionide.LanguageServerProtocol.Types +open ICSharpCode.Decompiler +open ICSharpCode.Decompiler.CSharp +open ICSharpCode.Decompiler.CSharp.Transforms + +open CSharpLanguageServer.Types +open CSharpLanguageServer.Util + + +let private processChange (oldText: SourceText) (change: TextChange) : TextEdit = + let mapToTextEdit (linePosition: LinePositionSpan, newText: string) : TextEdit = + { NewText = newText + Range = + { Start = + { Line = uint32 linePosition.Start.Line + Character = uint32 linePosition.Start.Character } + End = + { Line = uint32 linePosition.End.Line + Character = uint32 linePosition.End.Character } } } + + let defaultTextEdit (oldText: SourceText, change: TextChange) : TextEdit = + let linePosition = oldText.Lines.GetLinePositionSpan change.Span + mapToTextEdit (linePosition, change.NewText) + + let padLeft (span: TextSpan) : TextSpan = + TextSpan.FromBounds(span.Start - 1, span.End) + + let padRight (span: TextSpan) : TextSpan = + TextSpan.FromBounds(span.Start, span.End + 1) + + let rec checkSpanLineEndings (newText: string, oldText: SourceText, span: TextSpan, prefix: string) : TextEdit = + if span.Start > 0 && newText[0].Equals '\n' && oldText[span.Start - 1].Equals '\r' then + checkSpanLineEndings (newText, oldText, padLeft span, "\r") |> ignore + + if + span.End < oldText.Length - 1 + && newText[newText.Length - 1].Equals '\r' + && oldText[span.End].Equals '\n' + then + let linePosition = oldText.Lines.GetLinePositionSpan(padRight span) + mapToTextEdit (linePosition, prefix + newText.ToString() + "\n") + else + let linePosition = oldText.Lines.GetLinePositionSpan span + mapToTextEdit (linePosition, newText.ToString()) + + let newText = change.NewText + + if newText.Length > 0 then + checkSpanLineEndings (newText, oldText, change.Span, "") + else + defaultTextEdit (oldText, change) + + +let private convert (oldText: SourceText) (changes: TextChange[]) : TextEdit[] = + //why doesnt it pick up that TextSpan implements IComparable? + //one of life's many mysteries + let comparer (lhs: TextChange) (rhs: TextChange) : int = lhs.Span.CompareTo rhs.Span + + changes + |> Seq.sortWith comparer + |> Seq.map (fun x -> processChange oldText x) + |> Seq.toArray + + +let getDocumentDiffAsLspTextEdits (doc: Document) (oldDoc: Document) : Async = async { + let! ct = Async.CancellationToken + let! changes = doc.GetTextChangesAsync(oldDoc, ct) |> Async.AwaitTask + let! oldText = oldDoc.GetTextAsync ct |> Async.AwaitTask + return convert oldText (changes |> Seq.toArray) +} + + +let getDocumentFormattingOptionSet (doc: Document) (lspFormattingOptions: FormattingOptions option) : Async = async { + let! docOptions = doc.GetOptionsAsync() |> Async.AwaitTask + + return + match lspFormattingOptions with + | None -> docOptions + | Some lspFormattingOptions -> + docOptions + |> _.WithChangedOption( + FormattingOptions.IndentationSize, + LanguageNames.CSharp, + int lspFormattingOptions.TabSize + ) + |> _.WithChangedOption( + FormattingOptions.UseTabs, + LanguageNames.CSharp, + not lspFormattingOptions.InsertSpaces + ) + |> match lspFormattingOptions.InsertFinalNewline with + | Some insertFinalNewline -> + _.WithChangedOption(CSharpFormattingOptions.NewLineForFinally, insertFinalNewline) + | None -> id + |> match lspFormattingOptions.TrimFinalNewlines with + | Some trimFinalNewlines -> + _.WithChangedOption(CSharpFormattingOptions.NewLineForFinally, not trimFinalNewlines) + | None -> id +} + + +let documentFromMetadata + (compilation: Microsoft.CodeAnalysis.Compilation) + (project: Microsoft.CodeAnalysis.Project) + (l: Microsoft.CodeAnalysis.Location) + (fullName: string) + = + let mdLocation = l + + let containingAssembly = + mdLocation.MetadataModule + |> nonNull "mdLocation.MetadataModule" + |> _.ContainingAssembly + + let reference = + compilation.GetMetadataReference containingAssembly + |> nonNull "compilation.GetMetadataReference(containingAssembly)" + + let peReference = reference :?> PortableExecutableReference |> Option.ofObj + + let assemblyLocation = + peReference |> Option.map (fun r -> r.FilePath) |> Option.defaultValue "???" + + let decompilerSettings = DecompilerSettings() + decompilerSettings.ThrowOnAssemblyResolveErrors <- false // this shouldn't be a showstopper for us + + let decompiler = CSharpDecompiler(assemblyLocation, decompilerSettings) + + // Escape invalid identifiers to prevent Roslyn from failing to parse the generated code. + // (This happens for example, when there is compiler-generated code that is not yet recognized/transformed by the decompiler.) + decompiler.AstTransforms.Add(EscapeInvalidIdentifiers()) + + let fullTypeName = TypeSystem.FullTypeName fullName + + let text = decompiler.DecompileTypeAsString fullTypeName + + let mdDocumentFilename = + $"$metadata$/projects/{project.Name}/assemblies/{containingAssembly.Name}/symbols/{fullName}.cs" + + let mdDocumentEmpty = project.AddDocument(mdDocumentFilename, String.Empty) + + let mdDocument = SourceText.From text |> mdDocumentEmpty.WithText + mdDocument, text diff --git a/src/CSharpLanguageServer/Roslyn/Solution.fs b/src/CSharpLanguageServer/Roslyn/Solution.fs new file mode 100644 index 00000000..6ed2371e --- /dev/null +++ b/src/CSharpLanguageServer/Roslyn/Solution.fs @@ -0,0 +1,396 @@ +module CSharpLanguageServer.Roslyn.Solution + +open System +open System.IO +open System.Threading +open System.Collections.Generic +open System.Text.RegularExpressions + +open Ionide.LanguageServerProtocol +open Ionide.LanguageServerProtocol.Types +open Microsoft.Build.Construction +open Microsoft.Build.Exceptions +open Microsoft.Build.Locator +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.MSBuild +open Microsoft.CodeAnalysis.Text +open Microsoft.Extensions.Logging + +open CSharpLanguageServer +open CSharpLanguageServer.Lsp +open CSharpLanguageServer.Logging +open CSharpLanguageServer.Util +open CSharpLanguageServer.Roslyn.WorkspaceServices + + +let private logger = Logging.getLoggerByName "Roslyn.Solution" + + +let initializeMSBuild () : unit = + let vsInstanceQueryOpt = VisualStudioInstanceQueryOptions.Default + let vsInstanceList = MSBuildLocator.QueryVisualStudioInstances(vsInstanceQueryOpt) + + if Seq.isEmpty vsInstanceList then + raise ( + InvalidOperationException( + "No instances of MSBuild could be detected." + + Environment.NewLine + + "Try calling RegisterInstance or RegisterMSBuildPath to manually register one." + ) + ) + + logger.LogTrace("MSBuildLocator instances found:") + + for vsInstance in vsInstanceList do + logger.LogTrace( + sprintf + "- SDK=\"%s\", Version=%s, MSBuildPath=\"%s\", DiscoveryType=%s" + vsInstance.Name + (string vsInstance.Version) + vsInstance.MSBuildPath + (string vsInstance.DiscoveryType) + ) + + let vsInstance = vsInstanceList |> Seq.head + + logger.LogInformation( + "MSBuildLocator: will register \"{vsInstanceName}\", Version={vsInstanceVersion} as default instance", + vsInstance.Name, + (string vsInstance.Version) + ) + + MSBuildLocator.RegisterInstance(vsInstance) + + +let solutionLoadProjectFilenames (solutionPath: string) = + assert Path.IsPathRooted solutionPath + let projectFilenames = new List() + + let solutionFile = SolutionFile.Parse solutionPath + + for project in solutionFile.ProjectsInOrder do + if project.ProjectType = SolutionProjectType.KnownToBeMSBuildFormat then + projectFilenames.Add project.AbsolutePath + + projectFilenames |> Set.ofSeq + + +type TfmCategory = + | NetFramework of Version + | NetStandard of Version + | NetCoreApp of Version + | Net of Version + | Unknown + + +let selectLatestTfm (tfms: string seq) : string option = + let parseTfm (tfm: string) : TfmCategory = + let patterns = + [ @"^net(?\d)(?\d)?(?\d)?$", NetFramework + @"^netstandard(?\d+)\.(?\d+)$", NetStandard + @"^netcoreapp(?\d+)\.(?\d+)$", NetCoreApp + @"^net(?\d+)\.(?\d+)$", Net ] + + let matchingTfmCategory (pat, categoryCtor) = + let m = Regex.Match(tfm.ToLowerInvariant(), pat) + + if m.Success then + let readVersionNum (groupName: string) = + let group = m.Groups.[groupName] + if group.Success then int group.Value else 0 + + Version(readVersionNum "major", readVersionNum "minor", readVersionNum "build") + |> categoryCtor + |> Some + else + None + + patterns |> List.tryPick matchingTfmCategory |> Option.defaultValue Unknown + + let rankTfm = + function + | Net v -> 3000 + v.Major * 10 + v.Minor + | NetCoreApp v -> 2000 + v.Major * 10 + v.Minor + | NetStandard v -> 1000 + v.Major * 10 + v.Minor + | NetFramework v -> 0 + v.Major * 10 + v.Minor + | Unknown -> -1 + + tfms |> Seq.sortByDescending (parseTfm >> rankTfm) |> Seq.tryHead + + +let loadProjectTfms (projs: string seq) : Map> = + let mutable projectTfms = Map.empty + + for projectFilename in projs do + let projectCollection = new Microsoft.Build.Evaluation.ProjectCollection() + let props = new Dictionary() + + try + let buildProject = + projectCollection.LoadProject(projectFilename, props, toolsVersion = null) + + let noneIfEmpty s = + s + |> Option.ofObj + |> Option.bind (fun s -> if String.IsNullOrEmpty s then None else Some s) + + let targetFramework = + match buildProject.GetPropertyValue "TargetFramework" |> noneIfEmpty with + | Some tfm -> [ tfm.Trim() ] + | None -> [] + + let targetFrameworks = + match buildProject.GetPropertyValue "TargetFrameworks" |> noneIfEmpty with + | Some tfms -> tfms.Split ";" |> Array.map (fun s -> s.Trim()) |> List.ofArray + | None -> [] + + projectTfms <- projectTfms |> Map.add projectFilename (targetFramework @ targetFrameworks) + + projectCollection.UnloadProject buildProject + with :? InvalidProjectFileException as ipfe -> + logger.LogDebug( + "loadProjectTfms: failed to load {projectFilename}: {ex}", + projectFilename, + ipfe.GetType() |> string + ) + + projectTfms + + +let applyWorkspaceTargetFrameworkProp (tfmsPerProject: Map>) props : Map = + let selectedTfm = + match tfmsPerProject.Count with + | 0 -> None + | _ -> + tfmsPerProject.Values + |> Seq.map Set.ofSeq + |> Set.intersectMany + |> selectLatestTfm + + match selectedTfm with + | Some tfm -> props |> Map.add "TargetFramework" tfm + | None -> props + + +let resolveDefaultWorkspaceProps projs : Map = + let tfmsPerProject = loadProjectTfms projs + + Map.empty |> applyWorkspaceTargetFrameworkProp tfmsPerProject + +let solutionGetProjectForPath (solution: Solution) (filePath: string) : Project option = + let docDir = Path.GetDirectoryName filePath + + let fileOnProjectDir (p: Project) = + let projectDir = Path.GetDirectoryName p.FilePath + let projectDirWithDirSepChar = projectDir + string Path.DirectorySeparatorChar + + docDir = projectDir || docDir.StartsWith projectDirWithDirSepChar + + solution.Projects |> Seq.filter fileOnProjectDir |> Seq.tryHead + + +let solutionTryAddDocument (docFilePath: string) (text: string) (solution: Solution) : Async = async { + let projectOnPath = solutionGetProjectForPath solution docFilePath + + let! newDocumentMaybe = + match projectOnPath with + | Some proj -> + let projectBaseDir = Path.GetDirectoryName proj.FilePath + let docName = docFilePath.Substring(projectBaseDir.Length + 1) + + let newDoc = + proj.AddDocument(name = docName, text = SourceText.From text, folders = null, filePath = docFilePath) + + Some newDoc |> async.Return + + | None -> async { + logger.LogTrace("No parent project could be resolved to add file \"{file}\" to workspace", docFilePath) + return None + } + + return newDocumentMaybe +} + + +let selectPreferredSolution (slnFiles: string list) : option = + let getProjectCount (slnPath: string) = + try + let sln = SolutionFile.Parse slnPath + Some(sln.ProjectsInOrder.Count, slnPath) + with _ -> + None + + match slnFiles with + | [] -> None + | slnFiles -> + slnFiles + |> Seq.choose getProjectCount + |> Seq.sortByDescending fst + |> Seq.map snd + |> Seq.tryHead + + +let solutionTryLoadOnPath (lspClient: ILspClient) (solutionPath: string) = + assert Path.IsPathRooted solutionPath + let progress = ProgressReporter lspClient + + let logMessage m = + lspClient.WindowLogMessage + { Type = MessageType.Info + Message = sprintf "csharp-ls: %s" m } + + let showMessage m = + lspClient.WindowShowMessage + { Type = MessageType.Info + Message = sprintf "csharp-ls: %s" m } + + async { + try + let beginMessage = sprintf "Loading solution \"%s\"..." solutionPath + do! progress.Begin beginMessage + do! logMessage beginMessage + + let projs = solutionLoadProjectFilenames solutionPath + let workspaceProps = resolveDefaultWorkspaceProps projs + + if workspaceProps.Count > 0 then + logger.LogInformation("Will use these MSBuild props: {workspaceProps}", string workspaceProps) + + let msbuildWorkspace = + MSBuildWorkspace.Create(workspaceProps, CSharpLspHostServices()) + + msbuildWorkspace.LoadMetadataForReferencedProjects <- true + + let! solution = msbuildWorkspace.OpenSolutionAsync solutionPath |> Async.AwaitTask + + for diag in msbuildWorkspace.Diagnostics do + logger.LogInformation("msbuildWorkspace.Diagnostics: {message}", diag.ToString()) + + do! logMessage (sprintf "msbuildWorkspace.Diagnostics: %s" (diag.ToString())) + + let endMessage = sprintf "Finished loading solution \"%s\"" solutionPath + do! progress.End endMessage + do! logMessage endMessage + + return Some solution + with ex -> + let errorMessage = + sprintf "Solution \"%s\" could not be loaded: %s" solutionPath (ex.ToString()) + + do! progress.End errorMessage + do! showMessage errorMessage + return None + } + + +let solutionTryLoadFromProjectFiles (lspClient: ILspClient) (logMessage: string -> Async) (projs: string list) = + let progress = ProgressReporter lspClient + + async { + do! progress.Begin($"Loading {projs.Length} project(s)...", false, $"0/{projs.Length}", 0u) + let loadedProj = ref 0 + + let workspaceProps = resolveDefaultWorkspaceProps projs + + if workspaceProps.Count > 0 then + logger.LogDebug("Will use these MSBuild props: {workspaceProps}", string workspaceProps) + + let msbuildWorkspace = + MSBuildWorkspace.Create(workspaceProps, CSharpLspHostServices()) + + msbuildWorkspace.LoadMetadataForReferencedProjects <- true + + for file in projs do + if projs.Length < 10 then + do! logMessage (sprintf "loading project \"%s\".." file) + + try + do! msbuildWorkspace.OpenProjectAsync file |> Async.AwaitTask |> Async.Ignore + with ex -> + logger.LogError("could not OpenProjectAsync('{file}'): {exception}", file, string ex) + + let projectFile = new FileInfo(file) + let projName = projectFile.Name + let loaded = Interlocked.Increment loadedProj + let percent = 100 * loaded / projs.Length |> uint + do! progress.Report(false, $"{projName} {loaded}/{projs.Length}", percent) + + for diag in msbuildWorkspace.Diagnostics do + logger.LogTrace("msbuildWorkspace.Diagnostics: {message}", diag.ToString()) + + do! progress.End(sprintf "OK, %d project file(s) loaded" projs.Length) + + //workspace <- Some(msbuildWorkspace :> Workspace) + return Some msbuildWorkspace.CurrentSolution + } + + +let solutionFindAndLoadOnDir (lspClient: ILspClient) dir = async { + let fileNotOnNodeModules (filename: string) = + filename.Split Path.DirectorySeparatorChar |> Seq.contains "node_modules" |> not + + let solutionFiles = + [ "*.sln"; "*.slnx" ] + |> List.collect (fun p -> Directory.GetFiles(dir, p, SearchOption.AllDirectories) |> List.ofArray) + |> Seq.filter fileNotOnNodeModules + |> Seq.toList + + let logMessage m = + lspClient.WindowLogMessage + { Type = MessageType.Info + Message = sprintf "csharp-ls: %s" m } + + do! logMessage (sprintf "%d solution(s) found: [%s]" solutionFiles.Length (String.Join(", ", solutionFiles))) + + let preferredSlnFile = solutionFiles |> selectPreferredSolution + + match preferredSlnFile with + | None -> + do! + logMessage ( + "no single preferred .sln/.slnx file found on " + + dir + + "; fill load project files manually" + ) + + do! logMessage ("looking for .csproj/fsproj files on " + dir + "..") + + let projFiles = + let csprojFiles = Directory.GetFiles(dir, "*.csproj", SearchOption.AllDirectories) + let fsprojFiles = Directory.GetFiles(dir, "*.fsproj", SearchOption.AllDirectories) + + [ csprojFiles; fsprojFiles ] + |> Seq.concat + |> Seq.filter fileNotOnNodeModules + |> Seq.toList + + if projFiles.Length = 0 then + let message = "no or .csproj/.fsproj or sln files found on " + dir + do! logMessage message + Exception message |> raise + + return! solutionTryLoadFromProjectFiles lspClient logMessage projFiles + + | Some solutionPath -> return! solutionTryLoadOnPath lspClient solutionPath +} + + +let solutionLoadSolutionWithPathOrOnCwd (lspClient: ILspClient) (solutionPathMaybe: string option) (cwd: string) = + match solutionPathMaybe with + | Some solutionPath -> async { + let rootedSolutionPath = + match Path.IsPathRooted solutionPath with + | true -> solutionPath + | false -> Path.Combine(cwd, solutionPath) + + return! solutionTryLoadOnPath lspClient rootedSolutionPath + } + + | None -> async { + let logMessage: LogMessageParams = + { Type = MessageType.Info + Message = sprintf "csharp-ls: attempting to find and load solution based on cwd (\"%s\").." cwd } + + do! lspClient.WindowLogMessage logMessage + return! solutionFindAndLoadOnDir lspClient cwd + } diff --git a/src/CSharpLanguageServer/Roslyn/Symbol.fs b/src/CSharpLanguageServer/Roslyn/Symbol.fs new file mode 100644 index 00000000..c4e8fe04 --- /dev/null +++ b/src/CSharpLanguageServer/Roslyn/Symbol.fs @@ -0,0 +1,101 @@ +module CSharpLanguageServer.Roslyn.Symbol + +open System +open System.Collections.Generic +open System.IO +open System.Threading +open System.Text.RegularExpressions + +open Microsoft.Build.Locator +open ICSharpCode.Decompiler +open ICSharpCode.Decompiler.CSharp +open ICSharpCode.Decompiler.CSharp.Transforms +open Ionide.LanguageServerProtocol +open Ionide.LanguageServerProtocol.Types +open Microsoft.Build.Exceptions +open Microsoft.Build.Construction +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.CSharp +open Microsoft.CodeAnalysis.CSharp.Syntax +open Microsoft.CodeAnalysis.MSBuild +open Microsoft.CodeAnalysis.Text +open Microsoft.Extensions.Logging + +open CSharpLanguageServer +open CSharpLanguageServer.Roslyn.Conversions +open CSharpLanguageServer.Roslyn.WorkspaceServices +open CSharpLanguageServer.Roslyn.Solution +open CSharpLanguageServer.Util + + +type DocumentSymbolCollectorForMatchingSymbolName(documentUri, sym: ISymbol) = + inherit CSharpSyntaxWalker(SyntaxWalkerDepth.Token) + + let mutable collectedLocations = [] + let mutable suggestedLocations = [] + + let collectIdentifier (identifier: SyntaxToken) exactMatch = + let location: Types.Location = + { Uri = documentUri + Range = identifier.GetLocation().GetLineSpan().Span |> Range.fromLinePositionSpan } + + if exactMatch then + collectedLocations <- location :: collectedLocations + else + suggestedLocations <- location :: suggestedLocations + + member __.GetLocations() = + if not (Seq.isEmpty collectedLocations) then + collectedLocations |> Seq.rev |> List.ofSeq + else + suggestedLocations |> Seq.rev |> List.ofSeq + + override __.Visit(node: SyntaxNode | null) = + let node = node |> nonNull "node" + + match sym.Kind, node with + | SymbolKind.Method, (:? MethodDeclarationSyntax as m) when m.Identifier.ValueText = sym.Name -> + let symMethod = sym :?> IMethodSymbol + + let methodArityMatches = + symMethod.Parameters.Length = m.ParameterList.Parameters.Count + + collectIdentifier m.Identifier methodArityMatches + + | _, (:? TypeDeclarationSyntax as t) when t.Identifier.ValueText = sym.Name -> + collectIdentifier t.Identifier false + + | _, (:? PropertyDeclarationSyntax as p) when p.Identifier.ValueText = sym.Name -> + collectIdentifier p.Identifier false + + | _, (:? EventDeclarationSyntax as e) when e.Identifier.ValueText = sym.Name -> + collectIdentifier e.Identifier false + // TODO: collect other type of syntax nodes too + + | _ -> () + + + base.Visit node + + +let symbolGetContainingTypeOrThis (symbol: ISymbol) : INamedTypeSymbol = + if symbol :? INamedTypeSymbol then + symbol :?> INamedTypeSymbol + else + symbol.ContainingType + + +let symbolGetFullReflectionName (containingType: INamedTypeSymbol) = + let stack = Stack() + stack.Push containingType.MetadataName + let mutable ns = containingType.ContainingNamespace + + let mutable doContinue = true + + while doContinue do + stack.Push ns.Name + ns <- ns.ContainingNamespace + + doContinue <- ns <> null && not ns.IsGlobalNamespace + + String.Join(".", stack) diff --git a/src/CSharpLanguageServer/Roslyn/WorkspaceServices.fs b/src/CSharpLanguageServer/Roslyn/WorkspaceServices.fs new file mode 100644 index 00000000..5727a4ec --- /dev/null +++ b/src/CSharpLanguageServer/Roslyn/WorkspaceServices.fs @@ -0,0 +1,370 @@ +namespace CSharpLanguageServer.Roslyn.WorkspaceServices + +open System +open System.Collections.Generic +open System.Reflection +open System.Threading.Tasks +open System.Collections.Immutable + +open Castle.DynamicProxy +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.Host +open Microsoft.CodeAnalysis.Host.Mef +open Microsoft.Extensions.Logging + +open CSharpLanguageServer.Logging +open CSharpLanguageServer.Util + + +type CleanCodeGenerationOptionsProviderInterceptor(_logMessage) = + interface IInterceptor with + member __.Intercept(invocation: IInvocation) = + match invocation.Method.Name with + | "GetCleanCodeGenerationOptionsAsync" -> + let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" + + let cleanCodeGenOptionsType = + workspacesAssembly.GetType "Microsoft.CodeAnalysis.CodeGeneration.CleanCodeGenerationOptions" + |> nonNull + "workspacesAssembly.GetType('Microsoft.CodeAnalysis.CodeGeneration.CleanCodeGenerationOptions')" + + let getDefaultMI = + cleanCodeGenOptionsType.GetMethod "GetDefault" + |> nonNull "cleanCodeGenOptionsType.GetMethod('GetDefault')" + + let argLanguageServices = invocation.Arguments[0] + + let defaultCleanCodeGenOptions = + getDefaultMI.Invoke(null, [| argLanguageServices |]) + + let valueTaskType = typedefof> + + let valueTaskTypeForCleanCodeGenOptions = + valueTaskType.MakeGenericType [| cleanCodeGenOptionsType |] + + invocation.ReturnValue <- + Activator.CreateInstance(valueTaskTypeForCleanCodeGenOptions, defaultCleanCodeGenOptions) + + | _ -> NotImplementedException(string invocation.Method) |> raise + + +type CleanCodeGenOptionsProxy(logMessage) = + static let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" + static let generator = ProxyGenerator() + + static let cleanCodeGenOptionsProvTypeMaybe = + workspacesAssembly.GetType "Microsoft.CodeAnalysis.CodeGeneration.AbstractCleanCodeGenerationOptionsProvider" + |> Option.ofObj + + + member __.Create() = + let interceptor = CleanCodeGenerationOptionsProviderInterceptor logMessage + + let proxyMaybe = + cleanCodeGenOptionsProvTypeMaybe + |> Option.map (fun cleanCodeGenOptionsProvType -> + generator.CreateClassProxy(cleanCodeGenOptionsProvType, interceptor)) + + match proxyMaybe with + | Some proxy -> proxy + | None -> failwith "Could not create CleanCodeGenerationOptionsProvider proxy" + + +type LegacyWorkspaceOptionServiceInterceptor(logMessage) = + interface IInterceptor with + member __.Intercept(invocation: IInvocation) = + //logMessage (sprintf "LegacyWorkspaceOptionServiceInterceptor: %s" (string invocation.Method)) + + match invocation.Method.Name with + | "RegisterWorkspace" -> () + | "GetGenerateEqualsAndGetHashCodeFromMembersGenerateOperators" -> invocation.ReturnValue <- box true + | "GetGenerateEqualsAndGetHashCodeFromMembersImplementIEquatable" -> invocation.ReturnValue <- box true + | "GetGenerateConstructorFromMembersOptionsAddNullChecks" -> invocation.ReturnValue <- box true + | "get_GenerateOverrides" -> invocation.ReturnValue <- box true + | "get_CleanCodeGenerationOptionsProvider" -> + invocation.ReturnValue <- CleanCodeGenOptionsProxy(logMessage).Create() + | _ -> NotImplementedException(string invocation.Method) |> raise + + +type PickMembersServiceInterceptor(_logMessage) = + interface IInterceptor with + member __.Intercept(invocation: IInvocation) = + + match invocation.Method.Name with + | "PickMembers" -> + let argMembers = invocation.Arguments[1] + let argOptions = invocation.Arguments[2] + + let pickMembersResultType = invocation.Method.ReturnType + + invocation.ReturnValue <- + Activator.CreateInstance(pickMembersResultType, argMembers, argOptions, box true) + + | _ -> NotImplementedException(string invocation.Method) |> raise + + +type ExtractClassOptionsServiceInterceptor(_logMessage) = + + let getExtractClassOptionsImpl (argOriginalType: INamedTypeSymbol) : Object = + let featuresAssembly = Assembly.Load "Microsoft.CodeAnalysis.Features" + + let typeName = "Base" + argOriginalType.Name + let fileName = typeName + ".cs" + let sameFile = box true + + let immArrayType = typeof + + let extractClassMemberAnalysisResultType = + featuresAssembly.GetType "Microsoft.CodeAnalysis.ExtractClass.ExtractClassMemberAnalysisResult" + |> nonNull + "featuresAssembly.GetType('Microsoft.CodeAnalysis.ExtractClass.ExtractClassMemberAnalysisResult')" + + let resultListType = + typedefof>.MakeGenericType extractClassMemberAnalysisResultType + + let resultList = Activator.CreateInstance resultListType + + let memberFilter (m: ISymbol) = + match m with + | :? IMethodSymbol as ms -> ms.MethodKind = MethodKind.Ordinary + | :? IFieldSymbol as fs -> not fs.IsImplicitlyDeclared + | _ -> m.Kind = SymbolKind.Property || m.Kind = SymbolKind.Event + + let selectedMembersToAdd = argOriginalType.GetMembers() |> Seq.filter memberFilter + + for memberToAdd in selectedMembersToAdd do + let memberAnalysisResult = + Activator.CreateInstance(extractClassMemberAnalysisResultType, memberToAdd, false) + + let resultListAddMI = + resultListType.GetMethod "Add" |> nonNull "resultListType.GetMethod('Add')" + + resultListAddMI.Invoke(resultList, [| memberAnalysisResult |]) |> ignore + + let resultListToArrayMI = + resultListType.GetMethod "ToArray" + |> nonNull "resultListType.GetMethod('ToArray')" + + let resultListAsArray = resultListToArrayMI.Invoke(resultList, null) + + let immArrayCreateFromArrayMI = + immArrayType.GetMethods() + |> Seq.filter (fun m -> m.GetParameters().Length = 1 && (m.GetParameters()[0]).ParameterType.IsArray) + |> Seq.head + + let emptyMemberAnalysisResults = + immArrayCreateFromArrayMI + .MakeGenericMethod([| extractClassMemberAnalysisResultType |]) + .Invoke(null, [| resultListAsArray |]) + |> nonNull "MakeGenericMethod()" + + let extractClassOptionsType = + featuresAssembly.GetType "Microsoft.CodeAnalysis.ExtractClass.ExtractClassOptions" + |> nonNull "featuresAssembly.GetType('Microsoft.CodeAnalysis.ExtractClass.ExtractClassOptions')" + + Activator.CreateInstance(extractClassOptionsType, fileName, typeName, sameFile, emptyMemberAnalysisResults) + |> nonNull (sprintf "could not Activator.CreateInstance(%s,..)" (string extractClassOptionsType)) + + interface IInterceptor with + member __.Intercept(invocation: IInvocation) = + + match invocation.Method.Name with + | "GetExtractClassOptionsAsync" -> + let argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol + let extractClassOptionsValue = getExtractClassOptionsImpl argOriginalType + invocation.ReturnValue <- Task.fromResult (extractClassOptionsValue.GetType(), extractClassOptionsValue) + + | "GetExtractClassOptions" -> + let argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol + invocation.ReturnValue <- getExtractClassOptionsImpl argOriginalType + + | _ -> NotImplementedException(string invocation.Method) |> raise + + +type ExtractInterfaceOptionsServiceInterceptor(logMessage) = + interface IInterceptor with + + member __.Intercept(invocation: IInvocation) = + let argExtractableMembers, argDefaultInterfaceName = + match + invocation.Method.Name, invocation.Arguments[1], invocation.Arguments[2], invocation.Arguments[3] + with + | "GetExtractInterfaceOptions", + (:? ImmutableArray as extractableMembers), + (:? string as interfaceName), + _ -> extractableMembers, interfaceName + | "GetExtractInterfaceOptions", + _, + (:? ImmutableArray as extractableMembers), + (:? string as interfaceName) -> extractableMembers, interfaceName + | "GetExtractInterfaceOptionsAsync", + _, + (:? List as extractableMembers), + (:? string as interfaceName) -> extractableMembers.ToImmutableArray(), interfaceName + | _ -> NotImplementedException(string invocation.Method.Name) |> raise + + let fileName = sprintf "%s.cs" argDefaultInterfaceName + + let featuresAssembly = Assembly.Load "Microsoft.CodeAnalysis.Features" + + let extractInterfaceOptionsResultType = + featuresAssembly.GetType "Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult" + |> nonNull + "featuresAssembly.GetType('Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult')" + + let locationEnumType = + extractInterfaceOptionsResultType.GetNestedType "ExtractLocation" + |> nonNull "extractInterfaceOptionsResultType.GetNestedType('ExtractLocation')" + + let location = + Enum.Parse(locationEnumType, "NewFile") + |> fun v -> Convert.ChangeType(v, locationEnumType) + + invocation.ReturnValue <- + match invocation.Method.Name with + | "GetExtractInterfaceOptionsAsync" -> + Task.fromResult ( + extractInterfaceOptionsResultType, + Activator.CreateInstance( + extractInterfaceOptionsResultType, + false, // isCancelled + argExtractableMembers, + argDefaultInterfaceName, + fileName, + location, + CleanCodeGenOptionsProxy(logMessage).Create() + ) + ) + + | _ -> + Activator.CreateInstance( + extractInterfaceOptionsResultType, + false, // isCancelled + argExtractableMembers, + argDefaultInterfaceName, + fileName, + location + ) + + +type MoveStaticMembersOptionsServiceInterceptor(_logMessage) = + interface IInterceptor with + member __.Intercept(invocation: IInvocation) = + + match invocation.Method.Name with + | "GetMoveMembersToTypeOptions" -> + let _argDocument = invocation.Arguments[0] :?> Document + let _argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol + let argSelectedMembers = invocation.Arguments[2] :?> ImmutableArray + + let featuresAssembly = Assembly.Load "Microsoft.CodeAnalysis.Features" + + let msmOptionsType = + featuresAssembly.GetType "Microsoft.CodeAnalysis.MoveStaticMembers.MoveStaticMembersOptions" + |> nonNull "typeof" + + let newStaticClassName = "NewStaticClass" + + let msmOptions = + Activator.CreateInstance( + msmOptionsType, + newStaticClassName + ".cs", + newStaticClassName, + argSelectedMembers, + false |> box + ) + + invocation.ReturnValue <- msmOptions + + | _ -> NotImplementedException(string invocation.Method) |> raise + + +type RemoteHostClientProviderInterceptor(_logMessage) = + interface IInterceptor with + member __.Intercept(invocation: IInvocation) = + + match invocation.Method.Name with + | "TryGetRemoteHostClientAsync" -> + let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" + + let remoteHostClientType = + workspacesAssembly.GetType "Microsoft.CodeAnalysis.Remote.RemoteHostClient" + |> nonNull "GetType(Microsoft.CodeAnalysis.Remote.RemoteHostClient)" + + invocation.ReturnValue <- Task.fromResult (remoteHostClientType, null) + + | _ -> NotImplementedException(string invocation.Method) |> raise + + +type WorkspaceServicesInterceptor() = + let logger = Logging.getLoggerByName "WorkspaceServicesInterceptor" + + interface IInterceptor with + member __.Intercept(invocation: IInvocation) = + invocation.Proceed() + + if invocation.Method.Name = "GetService" && invocation.ReturnValue = null then + let updatedReturnValue = + let serviceType = invocation.GenericArguments[0] + let generator = ProxyGenerator() + + match serviceType.FullName with + | "Microsoft.CodeAnalysis.Options.ILegacyGlobalOptionsWorkspaceService" -> + let interceptor = LegacyWorkspaceOptionServiceInterceptor() + generator.CreateInterfaceProxyWithoutTarget(serviceType, interceptor) + + | "Microsoft.CodeAnalysis.PickMembers.IPickMembersService" -> + let interceptor = PickMembersServiceInterceptor() + generator.CreateInterfaceProxyWithoutTarget(serviceType, interceptor) + + | "Microsoft.CodeAnalysis.ExtractClass.IExtractClassOptionsService" -> + let interceptor = ExtractClassOptionsServiceInterceptor() + generator.CreateInterfaceProxyWithoutTarget(serviceType, interceptor) + + | "Microsoft.CodeAnalysis.ExtractInterface.IExtractInterfaceOptionsService" -> + let interceptor = ExtractInterfaceOptionsServiceInterceptor() + generator.CreateInterfaceProxyWithoutTarget(serviceType, interceptor) + + | "Microsoft.CodeAnalysis.MoveStaticMembers.IMoveStaticMembersOptionsService" -> + let interceptor = MoveStaticMembersOptionsServiceInterceptor() + generator.CreateInterfaceProxyWithoutTarget(serviceType, interceptor) + + | "Microsoft.CodeAnalysis.Remote.IRemoteHostClientProvider" -> + let interceptor = RemoteHostClientProviderInterceptor() + generator.CreateInterfaceProxyWithoutTarget(serviceType, interceptor) + + | "Microsoft.CodeAnalysis.SourceGeneratorTelemetry.ISourceGeneratorTelemetryCollectorWorkspaceService" + | "Microsoft.CodeAnalysis.CodeRefactorings.PullMemberUp.Dialog.IPullMemberUpOptionsService" + | "Microsoft.CodeAnalysis.Packaging.IPackageInstallerService" -> + // supress "GetService failed" messages for these services + null + + | _ -> + logger.LogDebug("GetService failed for {serviceType}", serviceType.FullName) + null + + invocation.ReturnValue <- updatedReturnValue + + +type CSharpLspHostServices() = + inherit HostServices() + + member private _.hostServices = MSBuildMefHostServices.DefaultServices + + override this.CreateWorkspaceServices(workspace: Workspace) = + // Ugly but we can't: + // 1. use Castle since there is no default constructor of MefHostServices. + // 2. call this.hostServices.CreateWorkspaceServices directly since it's internal. + let createWorkspaceServicesMI = + this.hostServices + .GetType() + .GetMethod("CreateWorkspaceServices", BindingFlags.Instance ||| BindingFlags.NonPublic) + |> nonNull (sprintf "no %s.CreateWorkspaceServices()" (this.hostServices.GetType() |> string)) + + let services = + createWorkspaceServicesMI.Invoke(this.hostServices, [| workspace |]) + |> Unchecked.unbox + + let generator = ProxyGenerator() + let interceptor = WorkspaceServicesInterceptor() + generator.CreateClassProxyWithTarget(services, interceptor) diff --git a/src/CSharpLanguageServer/RoslynHelpers.fs b/src/CSharpLanguageServer/RoslynHelpers.fs deleted file mode 100644 index b84a0baf..00000000 --- a/src/CSharpLanguageServer/RoslynHelpers.fs +++ /dev/null @@ -1,874 +0,0 @@ -module CSharpLanguageServer.RoslynHelpers - -open System -open System.Collections.Generic -open System.IO -open System.Reflection -open System.Threading -open System.Threading.Tasks -open System.Collections.Immutable -open System.Text.RegularExpressions - -open Microsoft.Build.Locator -open Castle.DynamicProxy -open ICSharpCode.Decompiler -open ICSharpCode.Decompiler.CSharp -open ICSharpCode.Decompiler.CSharp.Transforms -open Ionide.LanguageServerProtocol -open Ionide.LanguageServerProtocol.Types -open Microsoft.Build.Exceptions -open Microsoft.Build.Construction -open Microsoft.CodeAnalysis -open Microsoft.CodeAnalysis.CSharp -open Microsoft.CodeAnalysis.CSharp.Syntax -open Microsoft.CodeAnalysis.Host -open Microsoft.CodeAnalysis.Host.Mef -open Microsoft.CodeAnalysis.MSBuild -open Microsoft.CodeAnalysis.Text -open Microsoft.Extensions.Logging - -open CSharpLanguageServer -open CSharpLanguageServer.Conversions -open CSharpLanguageServer.Logging -open CSharpLanguageServer.Util - -type DocumentSymbolCollectorForMatchingSymbolName(documentUri, sym: ISymbol) = - inherit CSharpSyntaxWalker(SyntaxWalkerDepth.Token) - - let mutable collectedLocations = [] - let mutable suggestedLocations = [] - - let collectIdentifier (identifier: SyntaxToken) exactMatch = - let location: Types.Location = - { Uri = documentUri - Range = identifier.GetLocation().GetLineSpan().Span |> Range.fromLinePositionSpan } - - if exactMatch then - collectedLocations <- location :: collectedLocations - else - suggestedLocations <- location :: suggestedLocations - - member __.GetLocations() = - if not (Seq.isEmpty collectedLocations) then - collectedLocations |> Seq.rev |> List.ofSeq - else - suggestedLocations |> Seq.rev |> List.ofSeq - - override __.Visit(node: SyntaxNode | null) = - let node = node |> nonNull "node" - - match sym.Kind, node with - | SymbolKind.Method, (:? MethodDeclarationSyntax as m) when m.Identifier.ValueText = sym.Name -> - let symMethod = sym :?> IMethodSymbol - - let methodArityMatches = - symMethod.Parameters.Length = m.ParameterList.Parameters.Count - - collectIdentifier m.Identifier methodArityMatches - - | _, (:? TypeDeclarationSyntax as t) when t.Identifier.ValueText = sym.Name -> - collectIdentifier t.Identifier false - - | _, (:? PropertyDeclarationSyntax as p) when p.Identifier.ValueText = sym.Name -> - collectIdentifier p.Identifier false - - | _, (:? EventDeclarationSyntax as e) when e.Identifier.ValueText = sym.Name -> - collectIdentifier e.Identifier false - // TODO: collect other type of syntax nodes too - - | _ -> () - - - base.Visit node - -type CleanCodeGenerationOptionsProviderInterceptor(_logMessage) = - interface IInterceptor with - member __.Intercept(invocation: IInvocation) = - match invocation.Method.Name with - | "GetCleanCodeGenerationOptionsAsync" -> - let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" - - let cleanCodeGenOptionsType = - workspacesAssembly.GetType "Microsoft.CodeAnalysis.CodeGeneration.CleanCodeGenerationOptions" - |> nonNull - "workspacesAssembly.GetType('Microsoft.CodeAnalysis.CodeGeneration.CleanCodeGenerationOptions')" - - let getDefaultMI = - cleanCodeGenOptionsType.GetMethod "GetDefault" - |> nonNull "cleanCodeGenOptionsType.GetMethod('GetDefault')" - - let argLanguageServices = invocation.Arguments[0] - - let defaultCleanCodeGenOptions = - getDefaultMI.Invoke(null, [| argLanguageServices |]) - - let valueTaskType = typedefof> - - let valueTaskTypeForCleanCodeGenOptions = - valueTaskType.MakeGenericType [| cleanCodeGenOptionsType |] - - invocation.ReturnValue <- - Activator.CreateInstance(valueTaskTypeForCleanCodeGenOptions, defaultCleanCodeGenOptions) - - | _ -> NotImplementedException(string invocation.Method) |> raise - -type CleanCodeGenOptionsProxy(logMessage) = - static let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" - static let generator = ProxyGenerator() - - static let cleanCodeGenOptionsProvTypeMaybe = - workspacesAssembly.GetType "Microsoft.CodeAnalysis.CodeGeneration.AbstractCleanCodeGenerationOptionsProvider" - |> Option.ofObj - - - member __.Create() = - let interceptor = CleanCodeGenerationOptionsProviderInterceptor logMessage - - let proxyMaybe = - cleanCodeGenOptionsProvTypeMaybe - |> Option.map (fun cleanCodeGenOptionsProvType -> - generator.CreateClassProxy(cleanCodeGenOptionsProvType, interceptor)) - - match proxyMaybe with - | Some proxy -> proxy - | None -> failwith "Could not create CleanCodeGenerationOptionsProvider proxy" - -type LegacyWorkspaceOptionServiceInterceptor(logMessage) = - interface IInterceptor with - member __.Intercept(invocation: IInvocation) = - //logMessage (sprintf "LegacyWorkspaceOptionServiceInterceptor: %s" (string invocation.Method)) - - match invocation.Method.Name with - | "RegisterWorkspace" -> () - | "GetGenerateEqualsAndGetHashCodeFromMembersGenerateOperators" -> invocation.ReturnValue <- box true - | "GetGenerateEqualsAndGetHashCodeFromMembersImplementIEquatable" -> invocation.ReturnValue <- box true - | "GetGenerateConstructorFromMembersOptionsAddNullChecks" -> invocation.ReturnValue <- box true - | "get_GenerateOverrides" -> invocation.ReturnValue <- box true - | "get_CleanCodeGenerationOptionsProvider" -> - invocation.ReturnValue <- CleanCodeGenOptionsProxy(logMessage).Create() - | _ -> NotImplementedException(string invocation.Method) |> raise - -type PickMembersServiceInterceptor(_logMessage) = - interface IInterceptor with - member __.Intercept(invocation: IInvocation) = - - match invocation.Method.Name with - | "PickMembers" -> - let argMembers = invocation.Arguments[1] - let argOptions = invocation.Arguments[2] - - let pickMembersResultType = invocation.Method.ReturnType - - invocation.ReturnValue <- - Activator.CreateInstance(pickMembersResultType, argMembers, argOptions, box true) - - | _ -> NotImplementedException(string invocation.Method) |> raise - -type ExtractClassOptionsServiceInterceptor(_logMessage) = - - let getExtractClassOptionsImpl (argOriginalType: INamedTypeSymbol) : Object = - let featuresAssembly = Assembly.Load "Microsoft.CodeAnalysis.Features" - - let typeName = "Base" + argOriginalType.Name - let fileName = typeName + ".cs" - let sameFile = box true - - let immArrayType = typeof - - let extractClassMemberAnalysisResultType = - featuresAssembly.GetType "Microsoft.CodeAnalysis.ExtractClass.ExtractClassMemberAnalysisResult" - |> nonNull - "featuresAssembly.GetType('Microsoft.CodeAnalysis.ExtractClass.ExtractClassMemberAnalysisResult')" - - let resultListType = - typedefof>.MakeGenericType extractClassMemberAnalysisResultType - - let resultList = Activator.CreateInstance resultListType - - let memberFilter (m: ISymbol) = - match m with - | :? IMethodSymbol as ms -> ms.MethodKind = MethodKind.Ordinary - | :? IFieldSymbol as fs -> not fs.IsImplicitlyDeclared - | _ -> m.Kind = SymbolKind.Property || m.Kind = SymbolKind.Event - - let selectedMembersToAdd = argOriginalType.GetMembers() |> Seq.filter memberFilter - - for memberToAdd in selectedMembersToAdd do - let memberAnalysisResult = - Activator.CreateInstance(extractClassMemberAnalysisResultType, memberToAdd, false) - - let resultListAddMI = - resultListType.GetMethod "Add" |> nonNull "resultListType.GetMethod('Add')" - - resultListAddMI.Invoke(resultList, [| memberAnalysisResult |]) |> ignore - - let resultListToArrayMI = - resultListType.GetMethod "ToArray" - |> nonNull "resultListType.GetMethod('ToArray')" - - let resultListAsArray = resultListToArrayMI.Invoke(resultList, null) - - let immArrayCreateFromArrayMI = - immArrayType.GetMethods() - |> Seq.filter (fun m -> m.GetParameters().Length = 1 && (m.GetParameters()[0]).ParameterType.IsArray) - |> Seq.head - - let emptyMemberAnalysisResults = - immArrayCreateFromArrayMI - .MakeGenericMethod([| extractClassMemberAnalysisResultType |]) - .Invoke(null, [| resultListAsArray |]) - |> nonNull "MakeGenericMethod()" - - let extractClassOptionsType = - featuresAssembly.GetType "Microsoft.CodeAnalysis.ExtractClass.ExtractClassOptions" - |> nonNull "featuresAssembly.GetType('Microsoft.CodeAnalysis.ExtractClass.ExtractClassOptions')" - - Activator.CreateInstance(extractClassOptionsType, fileName, typeName, sameFile, emptyMemberAnalysisResults) - |> nonNull (sprintf "could not Activator.CreateInstance(%s,..)" (string extractClassOptionsType)) - - interface IInterceptor with - member __.Intercept(invocation: IInvocation) = - - match invocation.Method.Name with - | "GetExtractClassOptionsAsync" -> - let argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol - let extractClassOptionsValue = getExtractClassOptionsImpl argOriginalType - invocation.ReturnValue <- Task.fromResult (extractClassOptionsValue.GetType(), extractClassOptionsValue) - - | "GetExtractClassOptions" -> - let argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol - invocation.ReturnValue <- getExtractClassOptionsImpl argOriginalType - - | _ -> NotImplementedException(string invocation.Method) |> raise - -type ExtractInterfaceOptionsServiceInterceptor(logMessage) = - interface IInterceptor with - - member __.Intercept(invocation: IInvocation) = - let argExtractableMembers, argDefaultInterfaceName = - match - invocation.Method.Name, invocation.Arguments[1], invocation.Arguments[2], invocation.Arguments[3] - with - | "GetExtractInterfaceOptions", - (:? ImmutableArray as extractableMembers), - (:? string as interfaceName), - _ -> extractableMembers, interfaceName - | "GetExtractInterfaceOptions", - _, - (:? ImmutableArray as extractableMembers), - (:? string as interfaceName) -> extractableMembers, interfaceName - | "GetExtractInterfaceOptionsAsync", - _, - (:? List as extractableMembers), - (:? string as interfaceName) -> extractableMembers.ToImmutableArray(), interfaceName - | _ -> NotImplementedException(string invocation.Method.Name) |> raise - - let fileName = sprintf "%s.cs" argDefaultInterfaceName - - let featuresAssembly = Assembly.Load "Microsoft.CodeAnalysis.Features" - - let extractInterfaceOptionsResultType = - featuresAssembly.GetType "Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult" - |> nonNull - "featuresAssembly.GetType('Microsoft.CodeAnalysis.ExtractInterface.ExtractInterfaceOptionsResult')" - - let locationEnumType = - extractInterfaceOptionsResultType.GetNestedType "ExtractLocation" - |> nonNull "extractInterfaceOptionsResultType.GetNestedType('ExtractLocation')" - - let location = - Enum.Parse(locationEnumType, "NewFile") - |> fun v -> Convert.ChangeType(v, locationEnumType) - - invocation.ReturnValue <- - match invocation.Method.Name with - | "GetExtractInterfaceOptionsAsync" -> - Task.fromResult ( - extractInterfaceOptionsResultType, - Activator.CreateInstance( - extractInterfaceOptionsResultType, - false, // isCancelled - argExtractableMembers, - argDefaultInterfaceName, - fileName, - location, - CleanCodeGenOptionsProxy(logMessage).Create() - ) - ) - - | _ -> - Activator.CreateInstance( - extractInterfaceOptionsResultType, - false, // isCancelled - argExtractableMembers, - argDefaultInterfaceName, - fileName, - location - ) - -type MoveStaticMembersOptionsServiceInterceptor(_logMessage) = - interface IInterceptor with - member __.Intercept(invocation: IInvocation) = - - match invocation.Method.Name with - | "GetMoveMembersToTypeOptions" -> - let _argDocument = invocation.Arguments[0] :?> Document - let _argOriginalType = invocation.Arguments[1] :?> INamedTypeSymbol - let argSelectedMembers = invocation.Arguments[2] :?> ImmutableArray - - let featuresAssembly = Assembly.Load "Microsoft.CodeAnalysis.Features" - - let msmOptionsType = - featuresAssembly.GetType "Microsoft.CodeAnalysis.MoveStaticMembers.MoveStaticMembersOptions" - |> nonNull "typeof" - - let newStaticClassName = "NewStaticClass" - - let msmOptions = - Activator.CreateInstance( - msmOptionsType, - newStaticClassName + ".cs", - newStaticClassName, - argSelectedMembers, - false |> box - ) - - invocation.ReturnValue <- msmOptions - - | _ -> NotImplementedException(string invocation.Method) |> raise - -type RemoteHostClientProviderInterceptor(_logMessage) = - interface IInterceptor with - member __.Intercept(invocation: IInvocation) = - - match invocation.Method.Name with - | "TryGetRemoteHostClientAsync" -> - let workspacesAssembly = Assembly.Load "Microsoft.CodeAnalysis.Workspaces" - - let remoteHostClientType = - workspacesAssembly.GetType "Microsoft.CodeAnalysis.Remote.RemoteHostClient" - |> nonNull "GetType(Microsoft.CodeAnalysis.Remote.RemoteHostClient)" - - invocation.ReturnValue <- Task.fromResult (remoteHostClientType, null) - - | _ -> NotImplementedException(string invocation.Method) |> raise - -type WorkspaceServicesInterceptor() = - let logger = Logging.getLoggerByName "WorkspaceServicesInterceptor" - - interface IInterceptor with - member __.Intercept(invocation: IInvocation) = - invocation.Proceed() - - if invocation.Method.Name = "GetService" && invocation.ReturnValue = null then - let updatedReturnValue = - let serviceType = invocation.GenericArguments[0] - let generator = ProxyGenerator() - - match serviceType.FullName with - | "Microsoft.CodeAnalysis.Options.ILegacyGlobalOptionsWorkspaceService" -> - let interceptor = LegacyWorkspaceOptionServiceInterceptor() - generator.CreateInterfaceProxyWithoutTarget(serviceType, interceptor) - - | "Microsoft.CodeAnalysis.PickMembers.IPickMembersService" -> - let interceptor = PickMembersServiceInterceptor() - generator.CreateInterfaceProxyWithoutTarget(serviceType, interceptor) - - | "Microsoft.CodeAnalysis.ExtractClass.IExtractClassOptionsService" -> - let interceptor = ExtractClassOptionsServiceInterceptor() - generator.CreateInterfaceProxyWithoutTarget(serviceType, interceptor) - - | "Microsoft.CodeAnalysis.ExtractInterface.IExtractInterfaceOptionsService" -> - let interceptor = ExtractInterfaceOptionsServiceInterceptor() - generator.CreateInterfaceProxyWithoutTarget(serviceType, interceptor) - - | "Microsoft.CodeAnalysis.MoveStaticMembers.IMoveStaticMembersOptionsService" -> - let interceptor = MoveStaticMembersOptionsServiceInterceptor() - generator.CreateInterfaceProxyWithoutTarget(serviceType, interceptor) - - | "Microsoft.CodeAnalysis.Remote.IRemoteHostClientProvider" -> - let interceptor = RemoteHostClientProviderInterceptor() - generator.CreateInterfaceProxyWithoutTarget(serviceType, interceptor) - - | "Microsoft.CodeAnalysis.SourceGeneratorTelemetry.ISourceGeneratorTelemetryCollectorWorkspaceService" - | "Microsoft.CodeAnalysis.CodeRefactorings.PullMemberUp.Dialog.IPullMemberUpOptionsService" - | "Microsoft.CodeAnalysis.Packaging.IPackageInstallerService" -> - // supress "GetService failed" messages for these services - null - - | _ -> - logger.LogDebug("GetService failed for {serviceType}", serviceType.FullName) - null - - invocation.ReturnValue <- updatedReturnValue - -type CSharpLspHostServices() = - inherit HostServices() - - member private _.hostServices = MSBuildMefHostServices.DefaultServices - - override this.CreateWorkspaceServices(workspace: Workspace) = - // Ugly but we can't: - // 1. use Castle since there is no default constructor of MefHostServices. - // 2. call this.hostServices.CreateWorkspaceServices directly since it's internal. - let createWorkspaceServicesMI = - this.hostServices - .GetType() - .GetMethod("CreateWorkspaceServices", BindingFlags.Instance ||| BindingFlags.NonPublic) - |> nonNull (sprintf "no %s.CreateWorkspaceServices()" (this.hostServices.GetType() |> string)) - - let services = - createWorkspaceServicesMI.Invoke(this.hostServices, [| workspace |]) - |> Unchecked.unbox - - let generator = ProxyGenerator() - let interceptor = WorkspaceServicesInterceptor() - generator.CreateClassProxyWithTarget(services, interceptor) - - -let loadProjectFilenamesFromSolution (solutionPath: string) = - assert Path.IsPathRooted solutionPath - let projectFilenames = new List() - - let solutionFile = SolutionFile.Parse solutionPath - - for project in solutionFile.ProjectsInOrder do - if project.ProjectType = SolutionProjectType.KnownToBeMSBuildFormat then - projectFilenames.Add project.AbsolutePath - - projectFilenames |> Set.ofSeq - - -type TfmCategory = - | NetFramework of Version - | NetStandard of Version - | NetCoreApp of Version - | Net of Version - | Unknown - - -let selectLatestTfm (tfms: string seq) : string option = - let parseTfm (tfm: string) : TfmCategory = - let patterns = - [ @"^net(?\d)(?\d)?(?\d)?$", NetFramework - @"^netstandard(?\d+)\.(?\d+)$", NetStandard - @"^netcoreapp(?\d+)\.(?\d+)$", NetCoreApp - @"^net(?\d+)\.(?\d+)$", Net ] - - let matchingTfmCategory (pat, categoryCtor) = - let m = Regex.Match(tfm.ToLowerInvariant(), pat) - - if m.Success then - let readVersionNum (groupName: string) = - let group = m.Groups.[groupName] - if group.Success then int group.Value else 0 - - Version(readVersionNum "major", readVersionNum "minor", readVersionNum "build") - |> categoryCtor - |> Some - else - None - - patterns |> List.tryPick matchingTfmCategory |> Option.defaultValue Unknown - - let rankTfm = - function - | Net v -> 3000 + v.Major * 10 + v.Minor - | NetCoreApp v -> 2000 + v.Major * 10 + v.Minor - | NetStandard v -> 1000 + v.Major * 10 + v.Minor - | NetFramework v -> 0 + v.Major * 10 + v.Minor - | Unknown -> -1 - - tfms |> Seq.sortByDescending (parseTfm >> rankTfm) |> Seq.tryHead - - -let loadProjectTfms (logger: ILogger) (projs: string seq) : Map> = - let mutable projectTfms = Map.empty - - for projectFilename in projs do - let projectCollection = new Microsoft.Build.Evaluation.ProjectCollection() - let props = new Dictionary() - - try - let buildProject = - projectCollection.LoadProject(projectFilename, props, toolsVersion = null) - - let noneIfEmpty s = - s - |> Option.ofObj - |> Option.bind (fun s -> if String.IsNullOrEmpty s then None else Some s) - - let targetFramework = - match buildProject.GetPropertyValue "TargetFramework" |> noneIfEmpty with - | Some tfm -> [ tfm.Trim() ] - | None -> [] - - let targetFrameworks = - match buildProject.GetPropertyValue "TargetFrameworks" |> noneIfEmpty with - | Some tfms -> tfms.Split ";" |> Array.map (fun s -> s.Trim()) |> List.ofArray - | None -> [] - - projectTfms <- projectTfms |> Map.add projectFilename (targetFramework @ targetFrameworks) - - projectCollection.UnloadProject buildProject - with :? InvalidProjectFileException as ipfe -> - logger.LogDebug( - "loadProjectTfms: failed to load {projectFilename}: {ex}", - projectFilename, - ipfe.GetType() |> string - ) - - projectTfms - -let applyWorkspaceTargetFrameworkProp (tfmsPerProject: Map>) props : Map = - let selectedTfm = - match tfmsPerProject.Count with - | 0 -> None - | _ -> - tfmsPerProject.Values - |> Seq.map Set.ofSeq - |> Set.intersectMany - |> selectLatestTfm - - match selectedTfm with - | Some tfm -> props |> Map.add "TargetFramework" tfm - | None -> props - -let resolveDefaultWorkspaceProps (logger: ILogger) projs : Map = - let tfmsPerProject = loadProjectTfms logger projs - - Map.empty |> applyWorkspaceTargetFrameworkProp tfmsPerProject - - -let tryLoadSolutionOnPath (lspClient: ILspClient) (logger: ILogger) (solutionPath: string) = - assert Path.IsPathRooted solutionPath - let progress = ProgressReporter lspClient - - let logMessage m = - lspClient.WindowLogMessage - { Type = MessageType.Info - Message = sprintf "csharp-ls: %s" m } - - let showMessage m = - lspClient.WindowShowMessage - { Type = MessageType.Info - Message = sprintf "csharp-ls: %s" m } - - async { - try - let beginMessage = sprintf "Loading solution \"%s\"..." solutionPath - do! progress.Begin beginMessage - do! logMessage beginMessage - - let projs = loadProjectFilenamesFromSolution solutionPath - let workspaceProps = resolveDefaultWorkspaceProps logger projs - - if workspaceProps.Count > 0 then - logger.LogInformation("Will use these MSBuild props: {workspaceProps}", string workspaceProps) - - let msbuildWorkspace = - MSBuildWorkspace.Create(workspaceProps, CSharpLspHostServices()) - - msbuildWorkspace.LoadMetadataForReferencedProjects <- true - - let! solution = msbuildWorkspace.OpenSolutionAsync solutionPath |> Async.AwaitTask - - for diag in msbuildWorkspace.Diagnostics do - logger.LogInformation("msbuildWorkspace.Diagnostics: {message}", diag.ToString()) - - do! logMessage (sprintf "msbuildWorkspace.Diagnostics: %s" (diag.ToString())) - - let endMessage = sprintf "Finished loading solution \"%s\"" solutionPath - do! progress.End endMessage - do! logMessage endMessage - - return Some solution - with ex -> - let errorMessage = - sprintf "Solution \"%s\" could not be loaded: %s" solutionPath (ex.ToString()) - - do! progress.End errorMessage - do! showMessage errorMessage - return None - } - -let tryLoadSolutionFromProjectFiles - (lspClient: ILspClient) - (logger: ILogger) - (logMessage: string -> Async) - (projs: string list) - = - let progress = ProgressReporter lspClient - - async { - do! progress.Begin($"Loading {projs.Length} project(s)...", false, $"0/{projs.Length}", 0u) - let loadedProj = ref 0 - - let workspaceProps = resolveDefaultWorkspaceProps logger projs - - if workspaceProps.Count > 0 then - logger.LogDebug("Will use these MSBuild props: {workspaceProps}", string workspaceProps) - - let msbuildWorkspace = - MSBuildWorkspace.Create(workspaceProps, CSharpLspHostServices()) - - msbuildWorkspace.LoadMetadataForReferencedProjects <- true - - for file in projs do - if projs.Length < 10 then - do! logMessage (sprintf "loading project \"%s\".." file) - - try - do! msbuildWorkspace.OpenProjectAsync file |> Async.AwaitTask |> Async.Ignore - with ex -> - logger.LogError("could not OpenProjectAsync('{file}'): {exception}", file, string ex) - - let projectFile = new FileInfo(file) - let projName = projectFile.Name - let loaded = Interlocked.Increment loadedProj - let percent = 100 * loaded / projs.Length |> uint - do! progress.Report(false, $"{projName} {loaded}/{projs.Length}", percent) - - for diag in msbuildWorkspace.Diagnostics do - logger.LogTrace("msbuildWorkspace.Diagnostics: {message}", diag.ToString()) - - do! progress.End(sprintf "OK, %d project file(s) loaded" projs.Length) - - //workspace <- Some(msbuildWorkspace :> Workspace) - return Some msbuildWorkspace.CurrentSolution - } - -let selectPreferredSolution (slnFiles: string list) : option = - let getProjectCount (slnPath: string) = - try - let sln = SolutionFile.Parse slnPath - Some(sln.ProjectsInOrder.Count, slnPath) - with _ -> - None - - match slnFiles with - | [] -> None - | slnFiles -> - slnFiles - |> Seq.choose getProjectCount - |> Seq.sortByDescending fst - |> Seq.map snd - |> Seq.tryHead - -let findAndLoadSolutionOnDir (lspClient: ILspClient) (logger: ILogger) dir = async { - let fileNotOnNodeModules (filename: string) = - filename.Split Path.DirectorySeparatorChar |> Seq.contains "node_modules" |> not - - let solutionFiles = - [ "*.sln"; "*.slnx" ] - |> List.collect (fun p -> Directory.GetFiles(dir, p, SearchOption.AllDirectories) |> List.ofArray) - |> Seq.filter fileNotOnNodeModules - |> Seq.toList - - let logMessage m = - lspClient.WindowLogMessage - { Type = MessageType.Info - Message = sprintf "csharp-ls: %s" m } - - do! logMessage (sprintf "%d solution(s) found: [%s]" solutionFiles.Length (String.Join(", ", solutionFiles))) - - let preferredSlnFile = solutionFiles |> selectPreferredSolution - - match preferredSlnFile with - | None -> - do! - logMessage ( - "no single preferred .sln/.slnx file found on " - + dir - + "; fill load project files manually" - ) - - do! logMessage ("looking for .csproj/fsproj files on " + dir + "..") - - let projFiles = - let csprojFiles = Directory.GetFiles(dir, "*.csproj", SearchOption.AllDirectories) - let fsprojFiles = Directory.GetFiles(dir, "*.fsproj", SearchOption.AllDirectories) - - [ csprojFiles; fsprojFiles ] - |> Seq.concat - |> Seq.filter fileNotOnNodeModules - |> Seq.toList - - if projFiles.Length = 0 then - let message = "no or .csproj/.fsproj or sln files found on " + dir - do! logMessage message - Exception message |> raise - - return! tryLoadSolutionFromProjectFiles lspClient logger logMessage projFiles - - | Some solutionPath -> return! tryLoadSolutionOnPath lspClient logger solutionPath -} - -let loadSolutionOnSolutionPathOrDir - (lspClient: ILspClient) - (logger: ILogger) - (solutionPathMaybe: string option) - (rootPath: string) - = - match solutionPathMaybe with - | Some solutionPath -> async { - let rootedSolutionPath = - match Path.IsPathRooted solutionPath with - | true -> solutionPath - | false -> Path.Combine(rootPath, solutionPath) - - return! tryLoadSolutionOnPath lspClient logger rootedSolutionPath - } - - | None -> async { - let logMessage: LogMessageParams = - { Type = MessageType.Info - Message = sprintf "csharp-ls: attempting to find and load solution based on root path (\"%s\").." rootPath } - - do! lspClient.WindowLogMessage logMessage - return! findAndLoadSolutionOnDir lspClient logger rootPath - } - -let getContainingTypeOrThis (symbol: ISymbol) : INamedTypeSymbol = - if symbol :? INamedTypeSymbol then - symbol :?> INamedTypeSymbol - else - symbol.ContainingType - -let getFullReflectionName (containingType: INamedTypeSymbol) = - let stack = Stack() - stack.Push containingType.MetadataName - let mutable ns = containingType.ContainingNamespace - - let mutable doContinue = true - - while doContinue do - stack.Push ns.Name - ns <- ns.ContainingNamespace - - doContinue <- ns <> null && not ns.IsGlobalNamespace - - String.Join(".", stack) - -let getProjectForPathOnSolution (solution: Solution) (filePath: string) : Project option = - let docDir = Path.GetDirectoryName filePath - - let fileOnProjectDir (p: Project) = - let projectDir = Path.GetDirectoryName p.FilePath - let projectDirWithDirSepChar = projectDir + string Path.DirectorySeparatorChar - - docDir = projectDir || docDir.StartsWith projectDirWithDirSepChar - - solution.Projects |> Seq.filter fileOnProjectDir |> Seq.tryHead - -let tryAddDocument - (logger: ILogger) - (docFilePath: string) - (text: string) - (solution: Solution) - : Async = - async { - let projectOnPath = getProjectForPathOnSolution solution docFilePath - - let! newDocumentMaybe = - match projectOnPath with - | Some proj -> - let projectBaseDir = Path.GetDirectoryName proj.FilePath - let docName = docFilePath.Substring(projectBaseDir.Length + 1) - - let newDoc = - proj.AddDocument( - name = docName, - text = SourceText.From text, - folders = null, - filePath = docFilePath - ) - - Some newDoc |> async.Return - - | None -> async { - logger.LogTrace("No parent project could be resolved to add file \"{file}\" to workspace", docFilePath) - return None - } - - return newDocumentMaybe - } - -let makeDocumentFromMetadata - (compilation: Microsoft.CodeAnalysis.Compilation) - (project: Microsoft.CodeAnalysis.Project) - (l: Microsoft.CodeAnalysis.Location) - (fullName: string) - = - let mdLocation = l - - let containingAssembly = - mdLocation.MetadataModule - |> nonNull "mdLocation.MetadataModule" - |> _.ContainingAssembly - - let reference = - compilation.GetMetadataReference containingAssembly - |> nonNull "compilation.GetMetadataReference(containingAssembly)" - - let peReference = reference :?> PortableExecutableReference |> Option.ofObj - - let assemblyLocation = - peReference |> Option.map (fun r -> r.FilePath) |> Option.defaultValue "???" - - let decompilerSettings = DecompilerSettings() - decompilerSettings.ThrowOnAssemblyResolveErrors <- false // this shouldn't be a showstopper for us - - let decompiler = CSharpDecompiler(assemblyLocation, decompilerSettings) - - // Escape invalid identifiers to prevent Roslyn from failing to parse the generated code. - // (This happens for example, when there is compiler-generated code that is not yet recognized/transformed by the decompiler.) - decompiler.AstTransforms.Add(EscapeInvalidIdentifiers()) - - let fullTypeName = TypeSystem.FullTypeName fullName - - let text = decompiler.DecompileTypeAsString fullTypeName - - let mdDocumentFilename = - $"$metadata$/projects/{project.Name}/assemblies/{containingAssembly.Name}/symbols/{fullName}.cs" - - let mdDocumentEmpty = project.AddDocument(mdDocumentFilename, String.Empty) - - let mdDocument = SourceText.From text |> mdDocumentEmpty.WithText - mdDocument, text - - -let initializeMSBuild (logger: ILogger) : unit = - let vsInstanceQueryOpt = VisualStudioInstanceQueryOptions.Default - let vsInstanceList = MSBuildLocator.QueryVisualStudioInstances(vsInstanceQueryOpt) - - if Seq.isEmpty vsInstanceList then - raise ( - InvalidOperationException( - "No instances of MSBuild could be detected." - + Environment.NewLine - + "Try calling RegisterInstance or RegisterMSBuildPath to manually register one." - ) - ) - - logger.LogTrace("MSBuildLocator instances found:") - - for vsInstance in vsInstanceList do - logger.LogTrace( - sprintf - "- SDK=\"%s\", Version=%s, MSBuildPath=\"%s\", DiscoveryType=%s" - vsInstance.Name - (string vsInstance.Version) - vsInstance.MSBuildPath - (string vsInstance.DiscoveryType) - ) - - let vsInstance = vsInstanceList |> Seq.head - - logger.LogInformation( - "MSBuildLocator: will register \"{vsInstanceName}\", Version={vsInstanceVersion} as default instance", - vsInstance.Name, - (string vsInstance.Version) - ) - - MSBuildLocator.RegisterInstance(vsInstance) diff --git a/src/CSharpLanguageServer/State/ServerRequestContext.fs b/src/CSharpLanguageServer/State/ServerRequestContext.fs index 72e15f7e..6c6ed43c 100644 --- a/src/CSharpLanguageServer/State/ServerRequestContext.fs +++ b/src/CSharpLanguageServer/State/ServerRequestContext.fs @@ -3,12 +3,13 @@ namespace CSharpLanguageServer.State open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.FindSymbols open Ionide.LanguageServerProtocol.Types -open Microsoft.Extensions.Logging open CSharpLanguageServer.State.ServerState open CSharpLanguageServer.Types -open CSharpLanguageServer.RoslynHelpers -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Document +open CSharpLanguageServer.Roslyn.Symbol +open CSharpLanguageServer.Roslyn.Solution +open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Util open CSharpLanguageServer.Logging @@ -63,7 +64,7 @@ type ServerRequestContext(requestId: int, state: ServerState, emitServerEvent) = let! ct = Async.CancellationToken let! compilation = project.GetCompilationAsync(ct) |> Async.AwaitTask - let fullName = sym |> getContainingTypeOrThis |> getFullReflectionName + let fullName = sym |> symbolGetContainingTypeOrThis |> symbolGetFullReflectionName let containingAssemblyName = l.MetadataModule |> nonNull "l.MetadataModule" |> _.ContainingAssembly.Name @@ -75,7 +76,7 @@ type ServerRequestContext(requestId: int, state: ServerState, emitServerEvent) = match Map.tryFind uri state.DecompiledMetadata with | Some value -> (value.Document, []) | None -> - let (documentFromMd, text) = makeDocumentFromMetadata compilation project l fullName + let (documentFromMd, text) = documentFromMetadata compilation project l fullName let csharpMetadata = { ProjectName = project.Name diff --git a/src/CSharpLanguageServer/State/ServerState.fs b/src/CSharpLanguageServer/State/ServerState.fs index 19aaaf15..d41e6734 100644 --- a/src/CSharpLanguageServer/State/ServerState.fs +++ b/src/CSharpLanguageServer/State/ServerState.fs @@ -10,10 +10,11 @@ open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol open Microsoft.Extensions.Logging -open CSharpLanguageServer.RoslynHelpers -open CSharpLanguageServer.Types open CSharpLanguageServer.Logging -open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Roslyn.Conversions +open CSharpLanguageServer.Roslyn.Solution +open CSharpLanguageServer.Roslyn.Symbol +open CSharpLanguageServer.Types open CSharpLanguageServer.Util type DecompiledMetadataDocument = @@ -562,7 +563,7 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async match solutionReloadDeadline < DateTime.Now with | true -> let! newSolution = - loadSolutionOnSolutionPathOrDir state.LspClient.Value logger state.Settings.SolutionPath state.RootPath + solutionLoadSolutionWithPathOrOnCwd state.LspClient.Value state.Settings.SolutionPath state.RootPath return { state with diff --git a/src/CSharpLanguageServer/Types.fs b/src/CSharpLanguageServer/Types.fs index 28e1fd62..fc076242 100644 --- a/src/CSharpLanguageServer/Types.fs +++ b/src/CSharpLanguageServer/Types.fs @@ -11,6 +11,9 @@ type ServerSettings = ApplyFormattingOptions: bool DebugMode: bool } + member this.GetEffectiveFormattingOptions options = + if this.ApplyFormattingOptions then Some options else None + static member Default: ServerSettings = { SolutionPath = None LogLevel = LogLevel.Information diff --git a/src/CSharpLanguageServer/Util.fs b/src/CSharpLanguageServer/Util.fs index 55eb4470..45b74613 100644 --- a/src/CSharpLanguageServer/Util.fs +++ b/src/CSharpLanguageServer/Util.fs @@ -103,6 +103,15 @@ module Async = let map f computation = async.Bind(computation, f >> async.Return) + let bindOption f computation = + async.Bind( + computation, + fun v -> + match v with + | Some v -> f v + | None -> async.Return None + ) + module Map = let union map1 map2 = Map.fold (fun acc key value -> Map.add key value acc) map1 map2 diff --git a/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs b/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs index 0a400907..28da9dbc 100644 --- a/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs +++ b/tests/CSharpLanguageServer.Tests/DocumentFormattingTests.fs @@ -29,7 +29,7 @@ let testEditorConfigFormatting () = | Some tes -> let expectedClassContents = File - .ReadAllText(Path.Combine(client.ProjectDir, "Project", "ExpectedFormatting.cs.txt")) + .ReadAllText(Path.Combine(client.ProjectDir, "Project", "Class.cs.formatted.txt")) .ReplaceLineEndings("\n") let actualClassContents = diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/projectWithEditorConfig/Project/ExpectedFormatting.cs.txt b/tests/CSharpLanguageServer.Tests/Fixtures/projectWithEditorConfig/Project/Class.cs.formatted.txt similarity index 100% rename from tests/CSharpLanguageServer.Tests/Fixtures/projectWithEditorConfig/Project/ExpectedFormatting.cs.txt rename to tests/CSharpLanguageServer.Tests/Fixtures/projectWithEditorConfig/Project/Class.cs.formatted.txt diff --git a/tests/CSharpLanguageServer.Tests/InternalTests.fs b/tests/CSharpLanguageServer.Tests/InternalTests.fs index f6a2e1e8..ad310af1 100644 --- a/tests/CSharpLanguageServer.Tests/InternalTests.fs +++ b/tests/CSharpLanguageServer.Tests/InternalTests.fs @@ -3,7 +3,8 @@ module CSharpLanguageServer.Tests.InternalTests open System open NUnit.Framework -open CSharpLanguageServer.RoslynHelpers +open CSharpLanguageServer.Roslyn.Solution + [] [] @@ -37,6 +38,7 @@ let testApplyWorkspaceTargetFrameworkProp (tfmList: string, expectedTfm: string Assert.AreEqual(expectedTfm |> Option.ofObj, props |> Map.tryFind "TargetFramework") + [] let testApplyWorkspaceTargetFrameworkPropWithEmptyMap () = let props = Map.empty |> applyWorkspaceTargetFrameworkProp Map.empty