diff --git a/src/CSharpLanguageServer/CSharpLanguageServer.fsproj b/src/CSharpLanguageServer/CSharpLanguageServer.fsproj index 8b44f951..36e3ee10 100644 --- a/src/CSharpLanguageServer/CSharpLanguageServer.fsproj +++ b/src/CSharpLanguageServer/CSharpLanguageServer.fsproj @@ -17,7 +17,6 @@ README.md CHANGELOG.md enable - true diff --git a/src/CSharpLanguageServer/Handlers/Completion.fs b/src/CSharpLanguageServer/Handlers/Completion.fs index 899a374e..0f048d46 100644 --- a/src/CSharpLanguageServer/Handlers/Completion.fs +++ b/src/CSharpLanguageServer/Handlers/Completion.fs @@ -3,20 +3,25 @@ namespace CSharpLanguageServer.Handlers open System open System.Reflection +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.Text open Microsoft.Extensions.Caching.Memory open Ionide.LanguageServerProtocol.Server open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc +open Microsoft.Extensions.Logging open CSharpLanguageServer.State open CSharpLanguageServer.Util open CSharpLanguageServer.Roslyn.Conversions +open CSharpLanguageServer.Roslyn.Solution open CSharpLanguageServer.Logging open CSharpLanguageServer.Lsp.Workspace + [] module Completion = - let private _logger = Logging.getLoggerByName "Completion" + let private logger = Logging.getLoggerByName "Completion" let private completionItemMemoryCache = new MemoryCache(new MemoryCacheOptions()) @@ -181,16 +186,118 @@ module Completion = synopsis, documentationText | _, _ -> None, None - let handle + let codeActionContextToCompletionTrigger (context: CompletionContext option) = + 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 getCompletionsForRazorDocument + (p: CompletionParams) (context: ServerRequestContext) + : Async> = + async { + let wf = context.Workspace.SingletonFolder + + match! solutionGetRazorDocumentForUri wf.Solution.Value p.TextDocument.Uri with + | None -> return None + | Some(project, compilation, cshtmlTree) -> + let! ct = Async.CancellationToken + let! sourceText = cshtmlTree.GetTextAsync() |> Async.AwaitTask + + let razorTextDocument = + wf.Solution.Value + |> _.Projects + |> Seq.collect (fun p -> p.AdditionalDocuments) + |> Seq.filter (fun d -> Uri(d.FilePath, UriKind.Absolute) = Uri p.TextDocument.Uri) + |> Seq.head + + let! razorSourceText = razorTextDocument.GetTextAsync() |> Async.AwaitTask + + let posInCshtml = Position.toRoslynPosition sourceText.Lines p.Position + //logger.LogInformation("posInCshtml={posInCshtml=}", posInCshtml) + let pos = p.Position + + let root = cshtmlTree.GetRoot() + + let mutable positionAndToken: (int * SyntaxToken) option = None + + for t in root.DescendantTokens() do + let cshtmlSpan = cshtmlTree.GetMappedLineSpan(t.Span) + + if + cshtmlSpan.StartLinePosition.Line = (int pos.Line) + && cshtmlSpan.EndLinePosition.Line = (int pos.Line) + && cshtmlSpan.StartLinePosition.Character <= (int pos.Character) + then + let tokenStartCharacterOffset = + (int pos.Character - cshtmlSpan.StartLinePosition.Character) + + positionAndToken <- Some(t.Span.Start + tokenStartCharacterOffset, t) + + match positionAndToken with + | None -> return None + | Some(position, tokenForPosition) -> + + let newSourceText = + let cshtmlPosition = Position.toRoslynPosition razorSourceText.Lines p.Position + let charInCshtml: char = razorSourceText[cshtmlPosition - 1] + + if charInCshtml = '.' && string tokenForPosition.Value <> "." then + // a hack to make @Model.| autocompletion to work: + // - force a dot if present on .cscshtml but missing on .cs + sourceText.WithChanges(new TextChange(new TextSpan(position - 1, 0), ".")) + else + sourceText + + let cshtmlPath = Uri.toPath p.TextDocument.Uri + let! doc = solutionTryAddDocument (cshtmlPath + ".cs") (newSourceText.ToString()) wf.Solution.Value + + match doc with + | None -> return None + | Some doc -> + let completionService = + Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc) + |> RoslynCompletionServiceWrapper + + let completionOptions = + RoslynCompletionOptions.Default() + |> _.WithBool("ShowItemsFromUnimportedNamespaces", false) + |> _.WithBool("ShowNameSuggestions", false) + + let completionTrigger = p.Context |> codeActionContextToCompletionTrigger + + let! roslynCompletions = + completionService.GetCompletionsAsync( + doc, + position, + completionOptions, + completionTrigger, + ct + ) + |> Async.map Option.ofObj + + return roslynCompletions |> Option.map (fun rcl -> rcl, doc) + } + + let getCompletionsForCSharpDocument (p: CompletionParams) - : Async option>> = + (context: ServerRequestContext) + : Async> = async { let wf, docForUri = p.TextDocument.Uri |> workspaceDocument context.Workspace AnyDocument match docForUri with - | None -> return None |> LspResult.success + | None -> return None | Some doc -> let! ct = Async.CancellationToken let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask @@ -206,19 +313,7 @@ module Completion = |> _.WithBool("ShowItemsFromUnimportedNamespaces", false) |> _.WithBool("ShowNameSuggestions", false) - 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 completionTrigger = p.Context |> codeActionContextToCompletionTrigger let shouldTriggerCompletion = p.Context @@ -232,6 +327,23 @@ module Completion = else async.Return None + return roslynCompletions |> Option.map (fun rcl -> rcl, doc) + } + + let handle + (context: ServerRequestContext) + (p: CompletionParams) + : Async option>> = + async { + let getCompletions = + if p.TextDocument.Uri.EndsWith ".cshtml" then + getCompletionsForRazorDocument + else + getCompletionsForCSharpDocument + + match! getCompletions p context with + | None -> return None |> LspResult.success + | Some(roslynCompletions, doc) -> let toLspCompletionItemsWithCacheInfo (completions: Microsoft.CodeAnalysis.Completion.CompletionList) = completions.ItemsList |> Seq.map (fun item -> (item, Guid.NewGuid() |> string)) @@ -248,22 +360,21 @@ module Completion = |> Array.ofSeq let lspCompletionItemsWithCacheInfo = - roslynCompletions |> Option.map toLspCompletionItemsWithCacheInfo + roslynCompletions |> toLspCompletionItemsWithCacheInfo // cache roslyn completion items - for (_, cacheItemId, roslynDoc, roslynItem) in - (lspCompletionItemsWithCacheInfo |> Option.defaultValue Array.empty) do + for _, cacheItemId, roslynDoc, roslynItem in lspCompletionItemsWithCacheInfo do completionItemMemoryCacheSet cacheItemId roslynDoc roslynItem + let items = + lspCompletionItemsWithCacheInfo |> Array.map (fun (item, _, _, _) -> item) + return - lspCompletionItemsWithCacheInfo - |> Option.map (fun itemsWithCacheInfo -> - itemsWithCacheInfo |> Array.map (fun (item, _, _, _) -> item)) - |> Option.map (fun items -> - { IsIncomplete = true - Items = items - ItemDefaults = None }) - |> Option.map U2.C2 + { IsIncomplete = true + Items = items + ItemDefaults = None } + |> U2.C2 + |> Some |> LspResult.success } @@ -276,7 +387,8 @@ module Completion = match roslynDocAndItemMaybe with | Some(doc, roslynCompletionItem) -> let completionService = - Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc) + doc + |> Microsoft.CodeAnalysis.Completion.CompletionService.GetService |> nonNull "Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc)" let! ct = Async.CancellationToken diff --git a/src/CSharpLanguageServer/Handlers/Diagnostic.fs b/src/CSharpLanguageServer/Handlers/Diagnostic.fs index 621b81d5..6a844729 100644 --- a/src/CSharpLanguageServer/Handlers/Diagnostic.fs +++ b/src/CSharpLanguageServer/Handlers/Diagnostic.fs @@ -11,12 +11,9 @@ open CSharpLanguageServer.Types open CSharpLanguageServer.Util open CSharpLanguageServer.Lsp.Workspace - [] module Diagnostic = - let provider - (clientCapabilities: ClientCapabilities) - : U2 option = + let provider (_cc: ClientCapabilities) : U2 option = let registrationOptions: DiagnosticRegistrationOptions = { DocumentSelector = Some defaultDocumentSelector WorkDoneProgress = None @@ -38,27 +35,18 @@ module Diagnostic = Items = [||] RelatedDocuments = None } - let wf, docForUri = - p.TextDocument.Uri |> workspaceDocument context.Workspace AnyDocument - - match docForUri with - | None -> return emptyReport |> U2.C1 |> LspResult.success + let! wf, semanticModel = p.TextDocument.Uri |> workspaceDocumentSemanticModel context.Workspace - | Some doc -> - let! ct = Async.CancellationToken - let! semanticModelMaybe = doc.GetSemanticModelAsync(ct) |> Async.AwaitTask - - match semanticModelMaybe |> Option.ofObj with + let diagnostics = + match semanticModel with + | None -> [||] | Some semanticModel -> - let diagnostics = - semanticModel.GetDiagnostics() - |> Seq.map Diagnostic.fromRoslynDiagnostic - |> Seq.map fst - |> Array.ofSeq - - return { emptyReport with Items = diagnostics } |> U2.C1 |> LspResult.success + semanticModel.GetDiagnostics() + |> Seq.map Diagnostic.fromRoslynDiagnostic + |> Seq.map fst + |> Array.ofSeq - | None -> return emptyReport |> U2.C1 |> LspResult.success + return { emptyReport with Items = diagnostics } |> U2.C1 |> LspResult.success } let private getWorkspaceDiagnosticReports diff --git a/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs b/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs index 73393d61..695e0edf 100644 --- a/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs +++ b/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs @@ -21,46 +21,46 @@ module DocumentHighlight = | :? INamespaceSymbol -> false | _ -> true - let handle - (context: ServerRequestContext) - (p: DocumentHighlightParams) - : AsyncLspResult = - async { - let! ct = Async.CancellationToken - let filePath = Uri.toPath p.TextDocument.Uri + // We only need to find references in the file (not the whole workspace), so we don't use + // context.FindSymbol & context.FindReferences here. + let private getHighlights symbol (project: Project) (docMaybe: Document option) (filePath: string) = async { + let! ct = Async.CancellationToken - // We only need to find references in the file (not the whole workspace), so we don't use - // context.FindSymbol & context.FindReferences here. - let getHighlights (symbol: ISymbol) (doc: Document) = async { - let docSet = ImmutableHashSet.Create(doc) + let docSet: ImmutableHashSet option = + docMaybe |> Option.map (fun doc -> ImmutableHashSet.Create(doc)) - let! refs = - SymbolFinder.FindReferencesAsync(symbol, doc.Project.Solution, docSet, cancellationToken = ct) - |> Async.AwaitTask + let! refs = + SymbolFinder.FindReferencesAsync(symbol, project.Solution, docSet |> Option.toObj, cancellationToken = ct) + |> Async.AwaitTask - let! def = - SymbolFinder.FindSourceDefinitionAsync(symbol, doc.Project.Solution, cancellationToken = ct) - |> Async.AwaitTask + let! def = + SymbolFinder.FindSourceDefinitionAsync(symbol, project.Solution, cancellationToken = ct) + |> Async.AwaitTask - let locations = - refs - |> Seq.collect (fun r -> r.Locations) - |> Seq.map (fun rl -> rl.Location) - |> Seq.filter (fun l -> l.IsInSource && l.GetMappedLineSpan().Path = filePath) - |> Seq.append (def |> Option.ofObj |> Option.toList |> Seq.collect (fun sym -> sym.Locations)) + let locations = + refs + |> Seq.collect (fun r -> r.Locations) + |> Seq.map (fun rl -> rl.Location) + |> Seq.filter (fun l -> l.IsInSource && l.GetMappedLineSpan().Path = filePath) + |> Seq.append (def |> Option.ofObj |> Option.toList |> Seq.collect (fun sym -> sym.Locations)) - return - locations - |> Seq.choose Location.fromRoslynLocation - |> Seq.map (fun l -> - { Range = l.Range - Kind = Some DocumentHighlightKind.Read }) - } + return + locations + |> Seq.choose Location.fromRoslynLocation + |> Seq.map (fun l -> + { Range = l.Range + Kind = Some DocumentHighlightKind.Read }) + } + let handle + (context: ServerRequestContext) + (p: DocumentHighlightParams) + : AsyncLspResult = + async { match! workspaceDocumentSymbol context.Workspace AnyDocument p.TextDocument.Uri p.Position with - | Some wf, Some(symbol, _, Some doc) -> + | Some _wf, Some(symbol, project, docMaybe) -> if shouldHighlight symbol then - let! highlights = getHighlights symbol doc + let! highlights = getHighlights symbol project docMaybe (Uri.toPath p.TextDocument.Uri) return highlights |> Seq.toArray |> Some |> LspResult.success else return None |> LspResult.success diff --git a/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs b/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs index c113dcc3..da63e4c5 100644 --- a/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs +++ b/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs @@ -1,6 +1,8 @@ namespace CSharpLanguageServer.Handlers open System +open System.Text +open System.IO open Microsoft.CodeAnalysis.Text open Ionide.LanguageServerProtocol.Types @@ -12,19 +14,14 @@ open CSharpLanguageServer.State open CSharpLanguageServer.State.ServerState open CSharpLanguageServer.Roslyn.Solution open CSharpLanguageServer.Lsp.Workspace -open CSharpLanguageServer.Logging -open CSharpLanguageServer.Lsp.Workspace - +open CSharpLanguageServer.Util [] module TextDocumentSync = - let private logger = Logging.getLoggerByName "TextDocumentSync" - let private applyLspContentChangesOnRoslynSourceText (changes: TextDocumentContentChangeEvent[]) (initialSourceText: SourceText) = - let applyLspContentChangeOnRoslynSourceText (sourceText: SourceText) (change: TextDocumentContentChangeEvent) = match change with | U2.C1 change -> @@ -34,103 +31,193 @@ module TextDocumentSync = |> sourceText.Lines.GetTextSpan TextChange(changeTextSpan, change.Text) |> sourceText.WithChanges - | U2.C2 changeWoRange -> SourceText.From(changeWoRange.Text) + | U2.C2 changeWoRange -> SourceText.From changeWoRange.Text changes |> Seq.fold applyLspContentChangeOnRoslynSourceText initialSourceText - let provider (_: ClientCapabilities) : TextDocumentSyncOptions option = + let provider (_cc: ClientCapabilities) : TextDocumentSyncOptions option = { TextDocumentSyncOptions.Default with OpenClose = Some true Save = Some(U2.C2 { IncludeText = Some true }) Change = Some TextDocumentSyncKind.Incremental } |> Some + let didOpen (context: ServerRequestContext) (p: DidOpenTextDocumentParams) : Async> = async { + if p.TextDocument.Uri.EndsWith ".cshtml" then + let wf = context.Workspace.SingletonFolder + let u = p.TextDocument.Uri |> string + let uri = Uri(u.Replace("%3A", ":", true, null)) + + let matchingAdditionalDoc = + wf.Solution.Value + |> _.Projects + |> Seq.collect _.AdditionalDocuments + |> Seq.filter (fun d -> Uri(d.FilePath, UriKind.Absolute) = uri) + |> List.ofSeq + + let doc = + if matchingAdditionalDoc.Length = 1 then + matchingAdditionalDoc |> Seq.head |> Some + else + None + + let newSourceText = SourceText.From(p.TextDocument.Text, Encoding.UTF8) + + match doc with + | Some doc -> + let updatedWf = + doc.Project + |> _.RemoveAdditionalDocument(doc.Id) + |> _.AddAdditionalDocument(doc.Name, newSourceText, doc.Folders, doc.FilePath) + |> _.Project.Solution + |> (fun sln -> { wf with Solution = Some sln }) - let didOpen (context: ServerRequestContext) (p: DidOpenTextDocumentParams) : Async> = - let wf, docAndDocTypeForUri = - p.TextDocument.Uri |> workspaceDocumentDetails context.Workspace AnyDocument + context.Emit(WorkspaceFolderChange updatedWf) + context.Emit(DocumentOpened(p.TextDocument.Uri, p.TextDocument.Version, DateTime.Now)) - match wf, docAndDocTypeForUri with - | Some(wf), Some(doc, docType) -> - match docType with - | UserDocument -> - // we want to load the document in case it has been changed since we have the solution loaded - // also, as a bonus we can recover from corrupted document view in case document in roslyn solution - // went out of sync with editor - let updatedDoc = SourceText.From(p.TextDocument.Text) |> doc.WithText + | None -> + let cshtmlPath = Uri.toPath p.TextDocument.Uri + let project = solutionGetProjectForPath wf.Solution.Value cshtmlPath - context.Emit(DocumentOpened(p.TextDocument.Uri, p.TextDocument.Version, DateTime.Now)) + match project with + | Some project -> + let projectBaseDir = Path.GetDirectoryName project.FilePath + let relativePath = Path.GetRelativePath(projectBaseDir, cshtmlPath) - context.Emit( - WorkspaceFolderChange - { wf with - Solution = Some updatedDoc.Project.Solution } - ) + let folders = relativePath.Split Path.DirectorySeparatorChar - Ok() |> async.Return + let folders = folders |> Seq.take (folders.Length - 1) - | _ -> Ok() |> async.Return + let updatedWf = + project + |> _.AddAdditionalDocument(Path.GetFileName cshtmlPath, newSourceText, folders, cshtmlPath) + |> _.Project.Solution + |> (fun sln -> { wf with Solution = Some sln }) - | Some wf, None -> - let docFilePathMaybe = Util.tryParseFileUri p.TextDocument.Uri + context.Emit(WorkspaceFolderChange updatedWf) + context.Emit(DocumentOpened(p.TextDocument.Uri, p.TextDocument.Version, DateTime.Now)) - match docFilePathMaybe with - | Some docFilePath -> async { - // ok, this document is not in solution, register a new document - let! newDocMaybe = solutionTryAddDocument docFilePath p.TextDocument.Text wf.Solution.Value + | None -> () - match newDocMaybe with - | Some newDoc -> + return Ok() + else + let wf, docInfo = + workspaceDocumentDetails context.Workspace AnyDocument p.TextDocument.Uri + + match wf, docInfo with + | Some wf, Some(doc, docType) -> + match docType with + | UserDocument -> + // we want to load the document in case it has been changed since we have the solution loaded + // also, as a bonus we can recover from corrupted document view in case document in roslyn solution + // went out of sync with editor + + let updatedWf = + p.TextDocument.Text + |> SourceText.From + |> doc.WithText + |> _.Project.Solution + |> (fun sln -> { wf with Solution = Some sln }) + + context.Emit(WorkspaceFolderChange updatedWf) context.Emit(DocumentOpened(p.TextDocument.Uri, p.TextDocument.Version, DateTime.Now)) + return Ok() - context.Emit( - WorkspaceFolderChange - { wf with - Solution = Some newDoc.Project.Solution } - ) + | _ -> return Ok() - | None -> () + | Some wf, None -> + let docFilePathMaybe = Util.tryParseFileUri p.TextDocument.Uri - return Ok() - } + match docFilePathMaybe with + | Some docFilePath -> + // ok, this document is not in solution, register a new document + let! newDocMaybe = solutionTryAddDocument docFilePath p.TextDocument.Text wf.Solution.Value - | None -> Ok() |> async.Return + match newDocMaybe with + | None -> () + | Some newDoc -> + let updatedWf = + newDoc |> _.Project.Solution |> (fun sln -> { wf with Solution = Some sln }) - | _, _ -> Ok() |> async.Return + context.Emit(WorkspaceFolderChange updatedWf) + context.Emit(DocumentOpened(p.TextDocument.Uri, p.TextDocument.Version, DateTime.Now)) + + return Ok() + + | None -> return Ok() + + | _, _ -> return Ok() + } let didChange (context: ServerRequestContext) (p: DidChangeTextDocumentParams) : Async> = async { - let wf, docMaybe = - p.TextDocument.Uri |> workspaceDocument context.Workspace UserDocument + if p.TextDocument.Uri.EndsWith ".cshtml" then + let wf = context.Workspace.SingletonFolder + + let u = p.TextDocument.Uri |> string + let uri = Uri(u.Replace("%3A", ":", true, null)) + + let matchingAdditionalDoc = + wf.Solution.Value + |> _.Projects + |> Seq.collect _.AdditionalDocuments + |> Seq.filter (fun d -> Uri(d.FilePath, UriKind.Absolute) = uri) + |> List.ofSeq + + let doc = + if matchingAdditionalDoc.Length = 1 then + matchingAdditionalDoc |> Seq.head |> Some + else + None + + match doc with + | None -> () + | Some doc -> + let! ct = Async.CancellationToken + let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask - match wf, docMaybe with - | Some wf, Some doc -> - let! ct = Async.CancellationToken - let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask - //logMessage (sprintf "TextDocumentDidChange: changeParams: %s" (string changeParams)) - //logMessage (sprintf "TextDocumentDidChange: sourceText: %s" (string sourceText)) + let updatedSourceText = + sourceText |> applyLspContentChangesOnRoslynSourceText p.ContentChanges - let updatedSourceText = - sourceText |> applyLspContentChangesOnRoslynSourceText p.ContentChanges + let updatedSolution = + doc.Project + |> _.RemoveAdditionalDocument(doc.Id) + |> _.AddAdditionalDocument(doc.Name, updatedSourceText, doc.Folders, doc.FilePath) + |> _.Project.Solution + |> Some - let updatedDoc = doc.WithText(updatedSourceText) + let updatedWf = { wf with Solution = updatedSolution } + context.Emit(WorkspaceFolderChange updatedWf) + context.Emit(DocumentOpened(p.TextDocument.Uri, p.TextDocument.Version, DateTime.Now)) - //logMessage (sprintf "TextDocumentDidChange: newSourceText: %s" (string updatedSourceText)) + return Ok() + else + let wf, doc = workspaceDocument context.Workspace UserDocument p.TextDocument.Uri - let updatedWf = - { wf with - Solution = Some updatedDoc.Project.Solution } + match wf, doc with + | Some wf, Some doc -> + let! ct = Async.CancellationToken + let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask - context.Emit(WorkspaceFolderChange updatedWf) - context.Emit(DocumentOpened(p.TextDocument.Uri, p.TextDocument.Version, DateTime.Now)) + let updatedSolution = + sourceText + |> applyLspContentChangesOnRoslynSourceText p.ContentChanges + |> doc.WithText + |> _.Project.Solution + |> Some - | _, _ -> () + let updatedWf = { wf with Solution = updatedSolution } - return Ok() + context.Emit(WorkspaceFolderChange updatedWf) + context.Emit(DocumentOpened(p.TextDocument.Uri, p.TextDocument.Version, DateTime.Now)) + | _, _ -> () + + return Ok() } - let didClose (context: ServerRequestContext) (p: DidCloseTextDocumentParams) : Async> = + let didClose (context: ServerRequestContext) (p: DidCloseTextDocumentParams) : Async> = async { context.Emit(DocumentClosed p.TextDocument.Uri) - Ok() |> async.Return + return Ok() + } let willSave (_context: ServerRequestContext) (_p: WillSaveTextDocumentParams) : Async> = async { return Ok() @@ -142,30 +229,33 @@ module TextDocumentSync = : AsyncLspResult = async { return LspResult.notImplemented } - let didSave (context: ServerRequestContext) (p: DidSaveTextDocumentParams) : Async> = - let wf, doc = p.TextDocument.Uri |> workspaceDocument context.Workspace AnyDocument + let didSave (context: ServerRequestContext) (p: DidSaveTextDocumentParams) : Async> = async { + if p.TextDocument.Uri.EndsWith ".cshtml" then + return Ok() + else + let wf, doc = p.TextDocument.Uri |> workspaceDocument context.Workspace AnyDocument - match wf, doc with - | Some _, Some doc -> Ok() |> async.Return + match wf, doc with + | Some _, Some _ -> return Ok() - | Some wf, None -> async { - let docFilePath = Util.parseFileUri p.TextDocument.Uri + | Some wf, None -> + let docFilePath = Util.parseFileUri p.TextDocument.Uri - // we need to add this file to solution if not already - let! newDocMaybe = solutionTryAddDocument docFilePath p.Text.Value wf.Solution.Value + // we need to add this file to solution if not already + let! newDocMaybe = solutionTryAddDocument docFilePath p.Text.Value wf.Solution.Value - match newDocMaybe with - | Some newDoc -> - let updatedWf = - { wf with - Solution = Some newDoc.Project.Solution } + match newDocMaybe with + | Some newDoc -> + let updatedWf = + { wf with + Solution = Some newDoc.Project.Solution } - context.Emit(DocumentTouched(p.TextDocument.Uri, DateTime.Now)) - context.Emit(WorkspaceFolderChange updatedWf) + context.Emit(DocumentTouched(p.TextDocument.Uri, DateTime.Now)) + context.Emit(WorkspaceFolderChange updatedWf) - | None -> () + | None -> () - return Ok() - } + return Ok() - | _, _ -> Ok() |> async.Return + | _, _ -> return Ok() + } diff --git a/src/CSharpLanguageServer/Lsp/Workspace.fs b/src/CSharpLanguageServer/Lsp/Workspace.fs index d56357f0..294e0faa 100644 --- a/src/CSharpLanguageServer/Lsp/Workspace.fs +++ b/src/CSharpLanguageServer/Lsp/Workspace.fs @@ -13,6 +13,7 @@ open CSharpLanguageServer.Types open CSharpLanguageServer.Logging open CSharpLanguageServer.Roslyn.Document open CSharpLanguageServer.Roslyn.Symbol +open CSharpLanguageServer.Roslyn.Solution open CSharpLanguageServer.Roslyn.Conversions let logger = Logging.getLoggerByName "Lsp.Workspace" @@ -214,23 +215,55 @@ let workspaceDocument workspace docType (u: string) = let doc = docAndType |> Option.map fst wf, doc -let workspaceDocumentSymbol workspace docType (uri: DocumentUri) (pos: Ionide.LanguageServerProtocol.Types.Position) = async { - let wf, docForUri = uri |> workspaceDocument workspace AnyDocument +let workspaceDocumentSemanticModel (workspace: LspWorkspace) (uri: DocumentUri) = async { + if uri.EndsWith ".cshtml" then + let wf = workspace.SingletonFolder - match wf, docForUri with - | Some wf, Some doc -> - let! ct = Async.CancellationToken - let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask - let position = Position.toRoslynPosition sourceText.Lines pos - let! symbol = SymbolFinder.FindSymbolAtPositionAsync(doc, position, ct) |> Async.AwaitTask + match! solutionGetRazorDocumentForUri workspace.SingletonFolder.Solution.Value uri with + | None -> return Some wf, None + | Some(_, compilation, cshtmlTree) -> + let semanticModel = compilation.GetSemanticModel(cshtmlTree) |> Option.ofObj + return Some wf, semanticModel + else + let wf, docAndType = workspaceDocumentDetails workspace AnyDocument uri - let symbolInfo = - symbol |> Option.ofObj |> Option.map (fun sym -> sym, doc.Project, Some doc) - - return Some wf, symbolInfo + match docAndType with + | Some(doc, _) -> + let! ct = Async.CancellationToken + let! semanticModel = doc.GetSemanticModelAsync(ct) |> Async.AwaitTask |> Async.map Option.ofObj + return wf, semanticModel - | wf, _ -> return (wf, None) + | None -> return wf, None } +let workspaceDocumentSymbol + (workspace: LspWorkspace) + docType + (uri: DocumentUri) + (pos: Ionide.LanguageServerProtocol.Types.Position) + = + async { + if uri.EndsWith ".cshtml" then + let wf = workspace.SingletonFolder + let! symbolInfo = solutionFindSymbolForRazorDocumentUri wf.Solution.Value uri pos + return (Some wf, symbolInfo) + else + let wf, docForUri = uri |> workspaceDocument workspace docType + + match wf, docForUri with + | Some wf, Some doc -> + let! ct = Async.CancellationToken + let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask + let position = Position.toRoslynPosition sourceText.Lines pos + let! symbol = SymbolFinder.FindSymbolAtPositionAsync(doc, position, ct) |> Async.AwaitTask + + let symbolInfo = + symbol |> Option.ofObj |> Option.map (fun sym -> sym, doc.Project, Some doc) + + return Some wf, symbolInfo + + | wf, _ -> return (wf, None) + } + let workspaceDocumentVersion workspace uri = uri |> workspace.OpenDocs.TryFind |> Option.map _.Version diff --git a/src/CSharpLanguageServer/Roslyn/Solution.fs b/src/CSharpLanguageServer/Roslyn/Solution.fs index 8a7615c2..7dee5c68 100644 --- a/src/CSharpLanguageServer/Roslyn/Solution.fs +++ b/src/CSharpLanguageServer/Roslyn/Solution.fs @@ -18,6 +18,8 @@ open NuGet.Frameworks open CSharpLanguageServer.Lsp open CSharpLanguageServer.Logging +open CSharpLanguageServer.Util +open CSharpLanguageServer.Roslyn.Conversions open CSharpLanguageServer.Roslyn.WorkspaceServices let private logger = Logging.getLoggerByName "Roslyn.Solution" @@ -374,3 +376,80 @@ let solutionLoadSolutionWithPathOrOnCwd (lspClient: ILspClient) (solutionPathMay do! lspClient.WindowLogMessage logMessage return! solutionFindAndLoadOnDir lspClient cwd } + +let solutionGetRazorDocumentForUri + (solution: Solution) + (uri: string) + : Async<(Project * Compilation * SyntaxTree) option> = + async { + let cshtmlPath = uri |> Uri.toPath + let normalizedTargetDir = cshtmlPath |> Path.GetDirectoryName |> Path.GetFullPath + + let projectForPath = + solution.Projects + |> Seq.tryFind (fun project -> + let projectDirectory = Path.GetDirectoryName(project.FilePath) + let normalizedProjectDir = Path.GetFullPath(projectDirectory) + + normalizedTargetDir.StartsWith( + normalizedProjectDir + Path.DirectorySeparatorChar.ToString(), + StringComparison.OrdinalIgnoreCase + )) + + match projectForPath with + | None -> return None + | Some project -> + let projectBaseDir = Path.GetDirectoryName project.FilePath + + let! compilation = project.GetCompilationAsync() |> Async.AwaitTask + + let mutable cshtmlTree: SyntaxTree option = None + + let cshtmlPathTranslated = + Path.GetRelativePath(projectBaseDir, cshtmlPath) + |> _.Replace(".", "_") + |> _.Replace(Path.DirectorySeparatorChar, '_') + |> (fun s -> s + ".g.cs") + + for tree in compilation.SyntaxTrees do + let path = tree.FilePath + + if path.StartsWith projectBaseDir then + let relativePath = Path.GetRelativePath(projectBaseDir, path) + + if relativePath.EndsWith cshtmlPathTranslated then + cshtmlTree <- Some tree + + return cshtmlTree |> Option.map (fun cst -> (project, compilation, cst)) + } + +let solutionFindSymbolForRazorDocumentUri solution uri pos = async { + match! solutionGetRazorDocumentForUri solution uri with + | None -> return None + + | Some(project, compilation, cshtmlTree) -> + let model = compilation.GetSemanticModel cshtmlTree + + let root = cshtmlTree.GetRoot() + + let token = + let cshtmlPath = uri |> Uri.toPath + + root.DescendantTokens() + |> Seq.tryFind (fun t -> + let span = cshtmlTree.GetMappedLineSpan(t.Span) + + span.Path = cshtmlPath + && span.StartLinePosition.Line <= (int pos.Line) + && span.EndLinePosition.Line >= (int pos.Line) + && span.StartLinePosition.Character <= (int pos.Character) + && span.EndLinePosition.Character > (int pos.Character)) + + let symbol = + token + |> Option.bind (fun x -> x.Parent |> Option.ofObj) + |> Option.map (fun parentToken -> model.GetSymbolInfo(parentToken)) + |> Option.bind (fun x -> x.Symbol |> Option.ofObj) + + return symbol |> Option.map (fun sym -> (sym, project, None)) +} diff --git a/src/CSharpLanguageServer/State/ServerState.fs b/src/CSharpLanguageServer/State/ServerState.fs index 33398fd1..c7a21fc0 100644 --- a/src/CSharpLanguageServer/State/ServerState.fs +++ b/src/CSharpLanguageServer/State/ServerState.fs @@ -424,17 +424,42 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async let wf, docForUri = docUri |> workspaceDocument state.Workspace AnyDocument - match docForUri with - | None -> - // could not find document for this enqueued uri - logger.LogDebug( - "PushDiagnosticsProcessPendingDocuments: could not find document w/ uri \"{docUri}\"", - string docUri - ) + match wf, docForUri with + | Some wf, None -> + match! solutionGetRazorDocumentForUri wf.Solution.Value docUri with + | Some(_, compilation, cshtmlTree) -> + let semanticModelMaybe = compilation.GetSemanticModel cshtmlTree |> Option.ofObj + + match semanticModelMaybe with + | None -> + Error(Exception "could not GetSemanticModelAsync") + |> PushDiagnosticsDocumentDiagnosticsResolution + |> postSelf + + | Some semanticModel -> + let diagnostics = + semanticModel.GetDiagnostics() + |> Seq.map Diagnostic.fromRoslynDiagnostic + |> Seq.filter (fun (_, uri) -> uri = docUri) + |> Seq.map fst + |> Array.ofSeq + + Ok(docUri, None, diagnostics) + |> PushDiagnosticsDocumentDiagnosticsResolution + |> postSelf + + | None -> + // could not find document for this enqueued uri + logger.LogDebug( + "PushDiagnosticsProcessPendingDocuments: could not find document w/ uri \"{docUri}\"", + string docUri + ) + + () return newState - | Some doc -> + | Some wf, Some doc -> let resolveDocumentDiagnostics () : Task = task { let! semanticModelMaybe = doc.GetSemanticModelAsync() @@ -464,6 +489,8 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async return newState + | _, _ -> return newState + | _, _ -> // backlog is empty or pull diagnostics is enabled instead,--nothing to do return state diff --git a/src/CSharpLanguageServer/Types.fs b/src/CSharpLanguageServer/Types.fs index fc076242..9ecf6d5b 100644 --- a/src/CSharpLanguageServer/Types.fs +++ b/src/CSharpLanguageServer/Types.fs @@ -53,7 +53,9 @@ let razorCsharpDocumentFilter: TextDocumentFilter = Scheme = Some "file" Pattern = Some "**/*.cshtml" } -let defaultDocumentSelector: DocumentSelector = [| csharpDocumentFilter |> U2.C1 |] +// Type abbreviations cannot have augmentations, extensions +let defaultDocumentSelector: DocumentSelector = + [| csharpDocumentFilter |> U2.C1; razorCsharpDocumentFilter |> U2.C1 |] let emptyClientCapabilities: ClientCapabilities = { Workspace = None diff --git a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj index faa73635..9b9fbaac 100644 --- a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj +++ b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj @@ -11,22 +11,22 @@ + + + + + - - - - - + - - + diff --git a/tests/CSharpLanguageServer.Tests/CompletionTests.fs b/tests/CSharpLanguageServer.Tests/CompletionTests.fs index 24a65f77..67098993 100644 --- a/tests/CSharpLanguageServer.Tests/CompletionTests.fs +++ b/tests/CSharpLanguageServer.Tests/CompletionTests.fs @@ -133,6 +133,94 @@ let ``completion works for extension methods`` () = Some "(extension) string ClassForCompletionWithExtensionMethods.MethodB()" ) - Assert.IsFalse(itemResolved.Documentation.IsSome) + match itemResolved.Documentation with + | Some(U2.C2 markup) -> + Assert.AreEqual(MarkupKind.PlainText, markup.Kind) + Assert.IsNotEmpty(markup.Value) + | _ -> failwith "Documentation w/ Kind=Markdown was expected" + + () | _ -> failwith "Some U2.C1 was expected" + + +[] +let ``completion works in cshtml files`` () = + use client = activateFixture "aspnetProject" + + use cshtmlFile = client.Open("Project/Views/Test/CompletionTests.cshtml") + + let testCompletionResultContainsItem + line + character + expectedLabel + expectedCompletionItemKind + expectedDetail + documentationTestFn + = + let completionParams0: CompletionParams = + { TextDocument = { Uri = cshtmlFile.Uri } + Position = { Line = line; Character = character } + WorkDoneToken = None + PartialResultToken = None + Context = None } + + let completion: U2 option = + client.Request("textDocument/completion", completionParams0) + + match completion with + | Some(U2.C2 cl) -> + let expectedItem = cl.Items |> Seq.tryFind (fun i -> i.Label = expectedLabel) + + match expectedItem with + | None -> failwithf "an item with Label '%s' was expected for completion at this position" expectedLabel + | Some item -> + Assert.AreEqual(expectedLabel, item.Label) + Assert.IsFalse(item.Detail.IsSome) + Assert.IsFalse(item.Documentation.IsSome) + Assert.AreEqual(Some expectedCompletionItemKind, item.Kind) + + let itemResolved: CompletionItem = client.Request("completionItem/resolve", item) + + Assert.AreEqual(Some expectedDetail, itemResolved.Detail) + Assert.IsTrue(documentationTestFn itemResolved.Documentation) + + | _ -> failwith "Some U2.C1 was expected" + + // + // 1st completion test: (@Model.|) + // + testCompletionResultContainsItem + 1u + 14u + "Output" + CompletionItemKind.Property + "string? Project.Models.Test.IndexViewModel.Output { get; set; }" + _.IsNone + + // + // 2nd completion test: @Model.| + // + testCompletionResultContainsItem + 2u + 13u + "Output" + CompletionItemKind.Property + "string? Project.Models.Test.IndexViewModel.Output { get; set; }" + _.IsNone + + // + // 3nd completion test: @Model.Output.| + // + testCompletionResultContainsItem 3u 13u "ToString" CompletionItemKind.Method "string? object.ToString()" _.IsSome + + // + // 4nd completion test: x. + // + testCompletionResultContainsItem + 6u + 6u + "TryFormat" + CompletionItemKind.Method + "bool int.TryFormat(Span utf8Destination, out int bytesWritten, [ReadOnlySpan format = default], [IFormatProvider? provider = null])" + _.IsSome diff --git a/tests/CSharpLanguageServer.Tests/DiagnosticTests.fs b/tests/CSharpLanguageServer.Tests/DiagnosticTests.fs index 765ce983..39e13c0c 100644 --- a/tests/CSharpLanguageServer.Tests/DiagnosticTests.fs +++ b/tests/CSharpLanguageServer.Tests/DiagnosticTests.fs @@ -116,7 +116,37 @@ let testPullDiagnosticsWork () = Assert.AreEqual(0, report.Items.Length) | _ -> failwith "U2.C1 is expected" - () + +[] +let testPullDiagnosticsWorkForRazorFiles () = + use client = activateFixture "aspnetProject" + use cshtmlFile = client.Open("Project/Views/Test/Index.cshtml") + + let diagnosticParams: DocumentDiagnosticParams = + { WorkDoneToken = None + PartialResultToken = None + TextDocument = { Uri = cshtmlFile.Uri } + Identifier = None + PreviousResultId = None } + + let report0: DocumentDiagnosticReport option = + client.Request("textDocument/diagnostic", diagnosticParams) + + match report0 with + | Some(U2.C1 report) -> + Assert.AreEqual("full", report.Kind) + Assert.AreEqual(None, report.ResultId) + Assert.AreEqual(7, report.Items.Length) + + let reportItems = report.Items |> Array.sortBy _.Range + + let diagnostic0 = reportItems[0] + Assert.AreEqual(7, diagnostic0.Range.Start.Line) + Assert.AreEqual(4, diagnostic0.Range.Start.Character) + Assert.AreEqual(Some DiagnosticSeverity.Warning, diagnostic0.Severity) + Assert.AreEqual("Unnecessary using directive.", diagnostic0.Message) + + | _ -> failwith "U2.C1 is expected" [] @@ -155,7 +185,7 @@ let testWorkspaceDiagnosticsWork () = let testWorkspaceDiagnosticsWorkWithStreaming () = use client = activateFixture "testDiagnosticsWork" - Thread.Sleep(500) + Thread.Sleep(1000) let partialResultToken: ProgressToken = System.Guid.NewGuid() |> string |> U2.C2 diff --git a/tests/CSharpLanguageServer.Tests/DocumentHighlightTests.fs b/tests/CSharpLanguageServer.Tests/DocumentHighlightTests.fs index 999f1211..259d30b6 100644 --- a/tests/CSharpLanguageServer.Tests/DocumentHighlightTests.fs +++ b/tests/CSharpLanguageServer.Tests/DocumentHighlightTests.fs @@ -34,3 +34,26 @@ let ``test textDocument/documentHighlight works in .cs file`` () = Kind = Some DocumentHighlightKind.Read } ] Assert.AreEqual(Some expectedHighlights, highlights |> Option.map List.ofArray) + + +[] +let ``test textDocument/documentHighlight works in .cshtml file`` () = + use client = activateFixture "aspnetProject" + use indexCshtmlFile = client.Open("Project/Views/Test/Index.cshtml") + + let highlightParams: DocumentHighlightParams = + { TextDocument = { Uri = indexCshtmlFile.Uri } + Position = { Line = 1u; Character = 1u } + WorkDoneToken = None + PartialResultToken = None } + + let highlights: DocumentHighlight[] option = + client.Request("textDocument/documentHighlight", highlightParams) + + let expectedHighlights: DocumentHighlight list = + [ { Range = + { Start = { Line = 1u; Character = 1u } + End = { Line = 1u; Character = 6u } } + Kind = Some DocumentHighlightKind.Read } ] + + Assert.AreEqual(Some expectedHighlights, highlights |> Option.map List.ofArray) diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/aspnetProject/Project/Views/Test/CompletionTests.cshtml b/tests/CSharpLanguageServer.Tests/Fixtures/aspnetProject/Project/Views/Test/CompletionTests.cshtml new file mode 100644 index 00000000..3a22cf69 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/Fixtures/aspnetProject/Project/Views/Test/CompletionTests.cshtml @@ -0,0 +1,8 @@ +@model Project.Models.Test.IndexViewModel +(@Model.) +@Model. +@Model.Output. +@{ + var x = 1; + x. +} diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/ClassWithExtensionMethods.cs b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/ClassWithExtensionMethods.cs new file mode 100644 index 00000000..f1ba74c2 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/ClassWithExtensionMethods.cs @@ -0,0 +1,15 @@ +class ClassForCompletionWithExtensionMethods +{ + public void MethodA(string arg) + { + this. + } +} + +public static class ClassExtensions +{ + public static string MethodB(this ClassForCompletionWithExtensionMethods input) + { + return "ok"; + } +} diff --git a/tests/CSharpLanguageServer.Tests/HoverTests.fs b/tests/CSharpLanguageServer.Tests/HoverTests.fs index f1ce31eb..415460a2 100644 --- a/tests/CSharpLanguageServer.Tests/HoverTests.fs +++ b/tests/CSharpLanguageServer.Tests/HoverTests.fs @@ -47,19 +47,13 @@ let testHoverWorks () = Assert.IsTrue(hover1.IsSome) match hover1 with - | Some hover -> - match hover.Contents with - | U3.C1 c -> - Assert.AreEqual(MarkupKind.Markdown, c.Kind) - - Assert.AreEqual( - "```csharp\nstring\n```\n\nRepresents text as a sequence of UTF-16 code units.", - c.Value.ReplaceLineEndings("\n") - ) - | _ -> failwith "C1 was expected" - - Assert.IsTrue(hover.Range.IsNone) + | Some { Contents = U3.C1 c } -> + Assert.AreEqual(MarkupKind.Markdown, c.Kind) + Assert.AreEqual( + "```csharp\nstring\n```\n\nRepresents text as a sequence of UTF-16 code units.", + c.Value.ReplaceLineEndings("\n") + ) | _ -> failwith "Some (U3.C1 c) was expected" // @@ -73,3 +67,26 @@ let testHoverWorks () = let hover2: Hover option = client.Request("textDocument/hover", hover2Params) Assert.IsTrue(hover2.IsNone) + + +[] +let testHoverWorksInRazorFile () = + use client = activateFixture "aspnetProject" + + use indexCshtmlFile = client.Open("Project/Views/Test/Index.cshtml") + + let hover0Params: HoverParams = + { TextDocument = { Uri = indexCshtmlFile.Uri } + Position = { Line = 1u; Character = 7u } + WorkDoneToken = None } + + let hover0: Hover option = client.Request("textDocument/hover", hover0Params) + + Assert.IsTrue(hover0.IsSome) + + match hover0 with + | Some { Contents = U3.C1 c } -> + Assert.AreEqual(MarkupKind.Markdown, c.Kind) + Assert.AreEqual("```csharp\nstring? IndexViewModel.Output\n```", c.Value.ReplaceLineEndings("\n")) + + | _ -> failwith "Some (U3.C1 c) was expected" diff --git a/tests/CSharpLanguageServer.Tests/InitializationTests.fs b/tests/CSharpLanguageServer.Tests/InitializationTests.fs index 604bb791..c64f707b 100644 --- a/tests/CSharpLanguageServer.Tests/InitializationTests.fs +++ b/tests/CSharpLanguageServer.Tests/InitializationTests.fs @@ -67,13 +67,18 @@ let testServerRegistersCapabilitiesWithTheClient () = Assert.AreEqual(null, serverCaps.InlineValueProvider) + let expectedDocumentSelector = + [| U2.C1 + { Language = Some "csharp" + Scheme = Some "file" + Pattern = Some "**/*.cs" } + U2.C1 + { Language = Some "razor" + Scheme = Some "file" + Pattern = Some "**/*.cshtml" } |] + Assert.AreEqual( - { DocumentSelector = - Some - [| U2.C1 - { Language = Some "csharp" - Scheme = Some "file" - Pattern = Some "**/*.cs" } |] + { DocumentSelector = Some expectedDocumentSelector WorkDoneProgress = None Identifier = None InterFileDependencies = false diff --git a/tests/CSharpLanguageServer.Tests/ReferenceTests.fs b/tests/CSharpLanguageServer.Tests/ReferenceTests.fs index 07e7bba6..29d78ac9 100644 --- a/tests/CSharpLanguageServer.Tests/ReferenceTests.fs +++ b/tests/CSharpLanguageServer.Tests/ReferenceTests.fs @@ -142,14 +142,16 @@ let testReferenceWorksDotnet8 () = Assert.AreEqual(expectedLocations2, locations2.Value) - [] -let testReferenceWorksToAspNetRazorPageReferencedValue () = +let testReferenceWorksToRazorPageReferencedValue () = use client = activateFixture "aspnetProject" - use testIndexViewModelCsFile = client.Open("Project/Models/Test/IndexViewModel.cs") - use testControllerCsFile = client.Open("Project/Controllers/TestController.cs") - use viewsTestIndexCshtmlFile = client.Open("Project/Views/Test/Index.cshtml") + use testIndexViewModelCsFile = client.Open "Project/Models/Test/IndexViewModel.cs" + use testControllerCsFile = client.Open "Project/Controllers/TestController.cs" + use indexCshtmlFile = client.Open "Project/Views/Test/Index.cshtml" + + use completionTestsCshtmlFile = + client.Open "Project/Views/Test/CompletionTests.cshtml" let referenceParams0: ReferenceParams = { TextDocument = { Uri = testIndexViewModelCsFile.Uri } @@ -161,11 +163,21 @@ let testReferenceWorksToAspNetRazorPageReferencedValue () = let locations0: Location[] option = client.Request("textDocument/references", referenceParams0) - Assert.IsTrue(locations0.IsSome) - Assert.AreEqual(2, locations0.Value.Length) + Assert.IsTrue locations0.IsSome + Assert.AreEqual(3, locations0.Value.Length) let expectedLocations0: Location array = - [| { Uri = viewsTestIndexCshtmlFile.Uri + [| { Uri = testControllerCsFile.Uri + Range = + { Start = { Line = 11u; Character = 12u } + End = { Line = 11u; Character = 18u } } } + + { Uri = completionTestsCshtmlFile.Uri + Range = + { Start = { Line = 3u; Character = 13u } + End = { Line = 3u; Character = 19u } } } + + { Uri = indexCshtmlFile.Uri Range = { Start = { Line = 1u; Character = 7u } End = { Line = 1u; Character = 13u } } } @@ -177,7 +189,7 @@ let testReferenceWorksToAspNetRazorPageReferencedValue () = let sortedLocations0 = locations0.Value - |> Array.sortBy (fun f -> (f.Range.Start.Line, f.Range.Start.Character)) + |> Array.sortBy (fun f -> f.Range.Start.Line, f.Range.Start.Character) Assert.AreEqual(expectedLocations0, sortedLocations0) @@ -195,14 +207,19 @@ let testReferenceWorksToAspNetRazorPageReferencedValue () = client.Request("textDocument/references", referenceParams1) Assert.IsTrue(locations1.IsSome) - Assert.AreEqual(5, locations1.Value.Length) + Assert.AreEqual(6, locations1.Value.Length) let expectedLocations1: Location array = - [| { Uri = viewsTestIndexCshtmlFile.Uri + [| { Uri = indexCshtmlFile.Uri Range = { Start = { Line = 1u; Character = 7u } End = { Line = 1u; Character = 13u } } } + { Uri = completionTestsCshtmlFile.Uri + Range = + { Start = { Line = 3u; Character = 13u } + End = { Line = 3u; Character = 19u } } } + { Uri = testIndexViewModelCsFile.Uri Range = { Start = { Line = 3u; Character = 19u } @@ -228,3 +245,65 @@ let testReferenceWorksToAspNetRazorPageReferencedValue () = |> Array.sortBy (fun f -> (f.Range.Start.Line, f.Range.Start.Character)) Assert.AreEqual(expectedLocations1, sortedLocations1) + + +[] +let testReferenceWorksFromRazorPageReferencedValue () = + use client = activateFixture "aspnetProject" + + use testIndexViewModelCsFile = client.Open("Project/Models/Test/IndexViewModel.cs") + use testControllerCsFile = client.Open("Project/Controllers/TestController.cs") + use indexCshtmlFile = client.Open("Project/Views/Test/Index.cshtml") + + use completionTestsCshtmlFile = + client.Open("Project/Views/Test/CompletionTests.cshtml") + + let referenceParams0: ReferenceParams = + { TextDocument = { Uri = indexCshtmlFile.Uri } + Position = { Line = 1u; Character = 7u } + WorkDoneToken = None + PartialResultToken = None + Context = { IncludeDeclaration = true } } + + let locations0: Location[] option = + client.Request("textDocument/references", referenceParams0) + + Assert.IsTrue(locations0.IsSome) + Assert.AreEqual(6, locations0.Value.Length) + + let expectedLocations0: Location array = + [| { Uri = indexCshtmlFile.Uri + Range = + { Start = { Line = 1u; Character = 7u } + End = { Line = 1u; Character = 13u } } } + + { Uri = completionTestsCshtmlFile.Uri + Range = + { Start = { Line = 3u; Character = 13u } + End = { Line = 3u; Character = 19u } } } + + { Uri = testIndexViewModelCsFile.Uri + Range = + { Start = { Line = 3u; Character = 19u } + End = { Line = 3u; Character = 25u } } } + + { Uri = testIndexViewModelCsFile.Uri + Range = + { Start = { Line = 3u; Character = 28u } + End = { Line = 3u; Character = 31u } } } + + { Uri = testIndexViewModelCsFile.Uri + Range = + { Start = { Line = 3u; Character = 33u } + End = { Line = 3u; Character = 36u } } } + + { Uri = testControllerCsFile.Uri + Range = + { Start = { Line = 11u; Character = 12u } + End = { Line = 11u; Character = 18u } } } |] + + let sortedLocations0 = + locations0.Value + |> Array.sortBy (fun f -> (f.Range.Start.Line, f.Range.Start.Character)) + + Assert.AreEqual(expectedLocations0, sortedLocations0) diff --git a/tests/CSharpLanguageServer.Tests/WorkspaceSymbolTests.fs b/tests/CSharpLanguageServer.Tests/WorkspaceSymbolTests.fs index 8e1c1700..02b083f9 100644 --- a/tests/CSharpLanguageServer.Tests/WorkspaceSymbolTests.fs +++ b/tests/CSharpLanguageServer.Tests/WorkspaceSymbolTests.fs @@ -4,6 +4,7 @@ open NUnit.Framework open Ionide.LanguageServerProtocol.Types open CSharpLanguageServer.Tests.Tooling +open System.Threading [] let testWorkspaceSymbolWorks () = @@ -24,7 +25,7 @@ let testWorkspaceSymbolWorks () = match symbols0 with | Some(U2.C1 sis) -> - Assert.AreEqual(4, sis.Length) + Assert.AreEqual(6, sis.Length) let sym0 = sis[0] Assert.AreEqual("Class", sym0.Name)