diff --git a/src/CSharpLanguageServer/CSharpLanguageServer.fsproj b/src/CSharpLanguageServer/CSharpLanguageServer.fsproj index 17671f98..4e6b6045 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/Conversions.fs b/src/CSharpLanguageServer/Conversions.fs index 7078593e..426366cc 100644 --- a/src/CSharpLanguageServer/Conversions.fs +++ b/src/CSharpLanguageServer/Conversions.fs @@ -88,6 +88,8 @@ module Location = |> Option.bind (fun filePath -> if File.Exists filePath then Some filePath else None) |> Option.map (fun filePath -> toLspLocation filePath (loc.GetLineSpan().Span)) + //Console.Error.WriteLine("loc={0}; mapped={1}; source={2}", loc, mappedSourceLocation, sourceLocation) + mappedSourceLocation |> Option.orElse sourceLocation | _ -> None diff --git a/src/CSharpLanguageServer/Handlers/Completion.fs b/src/CSharpLanguageServer/Handlers/Completion.fs index 6d16fb7e..fb3b3eff 100644 --- a/src/CSharpLanguageServer/Handlers/Completion.fs +++ b/src/CSharpLanguageServer/Handlers/Completion.fs @@ -3,19 +3,23 @@ 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.Conversions open CSharpLanguageServer.Logging +open CSharpLanguageServer.RoslynHelpers [] module Completion = - let private _logger = Logging.getLoggerByName "Completion" + let private logger = Logging.getLoggerByName "Completion" let private completionItemMemoryCache = new MemoryCache(new MemoryCacheOptions()) @@ -180,13 +184,99 @@ module Completion = synopsis, documentationText | _, _ -> None, None - let handle + let getCompletionsForRazorDocument + (p: CompletionParams) (context: ServerRequestContext) + : Async> = + async { + match! getRazorDocumentForUri context.Solution p.TextDocument.Uri with + | None -> return None + | Some(project, compilation, cshtmlTree) -> + let! ct = Async.CancellationToken + let! sourceText = cshtmlTree.GetTextAsync() |> Async.AwaitTask + + let razorTextDocument = + context.Solution.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 = tryAddDocument logger context.Solution (cshtmlPath + ".cs") (newSourceText.ToString()) + + 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 = CompletionContext.toCompletionTrigger p.Context + + 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 { match context.GetDocument p.TextDocument.Uri with - | None -> return None |> LspResult.success + | None -> return None + | Some doc -> let! ct = Async.CancellationToken let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask @@ -216,6 +306,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)) @@ -232,26 +339,26 @@ 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 } let resolve (_context: ServerRequestContext) (item: CompletionItem) : AsyncLspResult = async { + let roslynDocAndItemMaybe = item.Data |> Option.bind deserialize @@ -259,6 +366,8 @@ module Completion = match roslynDocAndItemMaybe with | Some(doc, roslynCompletionItem) -> + logger.LogInformation("resolve, doc={0}, item={1}", doc, roslynCompletionItem) + let completionService = Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc) |> nonNull "Microsoft.CodeAnalysis.Completion.CompletionService.GetService(doc)" diff --git a/src/CSharpLanguageServer/Handlers/Diagnostic.fs b/src/CSharpLanguageServer/Handlers/Diagnostic.fs index 57fda17f..ef3e1e09 100644 --- a/src/CSharpLanguageServer/Handlers/Diagnostic.fs +++ b/src/CSharpLanguageServer/Handlers/Diagnostic.fs @@ -11,9 +11,7 @@ open CSharpLanguageServer.Types [] module Diagnostic = - let provider - (clientCapabilities: ClientCapabilities) - : U2 option = + let provider (_cc: ClientCapabilities) : U2 option = let registrationOptions: DiagnosticRegistrationOptions = { DocumentSelector = Some defaultDocumentSelector WorkDoneProgress = None @@ -35,24 +33,18 @@ module Diagnostic = Items = [||] RelatedDocuments = None } - match context.GetDocument p.TextDocument.Uri with - | None -> return emptyReport |> U2.C1 |> LspResult.success + let! semanticModel = context.GetSemanticModel p.TextDocument.Uri - | 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 56a01698..5c03786d 100644 --- a/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs +++ b/src/CSharpLanguageServer/Handlers/DocumentHighlight.fs @@ -12,53 +12,53 @@ open CSharpLanguageServer.Conversions [] module DocumentHighlight = - let provider (_: ClientCapabilities) : U2 option = Some(U2.C1 true) + let provider (_cc: ClientCapabilities) : U2 option = Some(U2.C1 true) let private shouldHighlight (symbol: ISymbol) = match symbol with | :? 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! context.FindSymbol' p.TextDocument.Uri p.Position with - | Some(symbol, _, Some doc) -> + | 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 1e7685d2..4641e984 100644 --- a/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs +++ b/src/CSharpLanguageServer/Handlers/TextDocumentSync.fs @@ -1,7 +1,10 @@ namespace CSharpLanguageServer.Handlers open System +open System.Text +open System.IO +open Microsoft.Extensions.Logging open Microsoft.CodeAnalysis.Text open Ionide.LanguageServerProtocol.Types open Ionide.LanguageServerProtocol.JsonRpc @@ -21,7 +24,6 @@ module TextDocumentSync = (changes: TextDocumentContentChangeEvent[]) (initialSourceText: SourceText) = - let applyLspContentChangeOnRoslynSourceText (sourceText: SourceText) (change: TextDocumentContentChangeEvent) = match change with | U2.C1 change -> @@ -42,74 +44,160 @@ module TextDocumentSync = Change = Some TextDocumentSyncKind.Incremental } |> Some - let didOpen (context: ServerRequestContext) (openParams: DidOpenTextDocumentParams) : Async> = - match context.GetDocumentForUriOfType AnyDocument openParams.TextDocument.Uri with - | 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(openParams.TextDocument.Text) |> doc.WithText + if openParams.TextDocument.Uri.EndsWith(".cshtml") then + let u = openParams.TextDocument.Uri |> string + let uri = Uri(u.Replace("%3A", ":", true, null)) + + let matchingAdditionalDoc = + context.Solution.Projects + |> Seq.collect (fun p -> p.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(openParams.TextDocument.Text, Encoding.UTF8) + + match doc with + | Some doc -> + let updatedDoc = + doc.Project + |> _.RemoveAdditionalDocument(doc.Id) + |> _.AddAdditionalDocument(doc.Name, newSourceText, doc.Folders, doc.FilePath) context.Emit(OpenDocAdd(openParams.TextDocument.Uri, openParams.TextDocument.Version, DateTime.Now)) context.Emit(SolutionChange updatedDoc.Project.Solution) - Ok() |> async.Return + | None -> + let cshtmlPath = Uri.toPath openParams.TextDocument.Uri + let project = getProjectForPathOnSolution context.Solution cshtmlPath - | _ -> Ok() |> async.Return + match project with + | Some project -> + let projectBaseDir = Path.GetDirectoryName(project.FilePath) + let relativePath = Path.GetRelativePath(projectBaseDir, cshtmlPath) - | None -> - let docFilePathMaybe = Util.tryParseFileUri openParams.TextDocument.Uri + let folders = relativePath.Split(Path.DirectorySeparatorChar) - 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 folders = folders |> Seq.take (folders.Length - 1) + + let newDoc = + project.AddAdditionalDocument(Path.GetFileName(cshtmlPath), newSourceText, folders, cshtmlPath) - match newDocMaybe with - | Some newDoc -> context.Emit(OpenDocAdd(openParams.TextDocument.Uri, openParams.TextDocument.Version, DateTime.Now)) context.Emit(SolutionChange newDoc.Project.Solution) + () | None -> () - return Ok() - } + Ok() |> async.Return + else + match context.GetDocumentForUriOfType AnyDocument openParams.TextDocument.Uri with + | 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(openParams.TextDocument.Text) |> doc.WithText + + context.Emit(OpenDocAdd(openParams.TextDocument.Uri, openParams.TextDocument.Version, DateTime.Now)) + context.Emit(SolutionChange updatedDoc.Project.Solution) + + Ok() |> async.Return + + | _ -> Ok() |> async.Return + + | None -> + let docFilePathMaybe = Util.tryParseFileUri openParams.TextDocument.Uri + + match docFilePathMaybe with + | Some docFilePath -> async { + // ok, this document is not in solution, register a new document + let! newDocMaybe = tryAddDocument logger context.Solution docFilePath openParams.TextDocument.Text + + match newDocMaybe with + | Some newDoc -> + context.Emit( + OpenDocAdd(openParams.TextDocument.Uri, openParams.TextDocument.Version, DateTime.Now) + ) + + context.Emit(SolutionChange newDoc.Project.Solution) + + | None -> () - | None -> Ok() |> async.Return + return Ok() + } + | None -> Ok() |> async.Return let didChange (context: ServerRequestContext) (changeParams: DidChangeTextDocumentParams) : Async> = async { - let docMaybe = context.GetUserDocument changeParams.TextDocument.Uri + if changeParams.TextDocument.Uri.EndsWith(".cshtml") then + let u = changeParams.TextDocument.Uri |> string + let uri = Uri(u.Replace("%3A", ":", true, null)) + + let matchingAdditionalDoc = + context.Solution.Projects + |> Seq.collect (fun p -> p.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 + + let updatedSourceText = + sourceText + |> applyLspContentChangesOnRoslynSourceText changeParams.ContentChanges - match docMaybe with - | None -> () - | 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 updatedDoc = + doc.Project + |> _.RemoveAdditionalDocument(doc.Id) + |> _.AddAdditionalDocument(doc.Name, updatedSourceText, doc.Folders, doc.FilePath) + + context.Emit(OpenDocAdd(changeParams.TextDocument.Uri, changeParams.TextDocument.Version, DateTime.Now)) + context.Emit(SolutionChange updatedDoc.Project.Solution) - let updatedSourceText = - sourceText - |> applyLspContentChangesOnRoslynSourceText changeParams.ContentChanges + return Ok() + else + let docMaybe = context.GetUserDocument changeParams.TextDocument.Uri - let updatedDoc = doc.WithText(updatedSourceText) + match docMaybe with + | None -> () + | Some doc -> + let! ct = Async.CancellationToken + let! sourceText = doc.GetTextAsync(ct) |> Async.AwaitTask - //logMessage (sprintf "TextDocumentDidChange: newSourceText: %s" (string updatedSourceText)) + let updatedSourceText = + sourceText + |> applyLspContentChangesOnRoslynSourceText changeParams.ContentChanges - let updatedSolution = updatedDoc.Project.Solution + let updatedSolution = doc.WithText(updatedSourceText).Project.Solution - context.Emit(SolutionChange updatedSolution) - context.Emit(OpenDocAdd(changeParams.TextDocument.Uri, changeParams.TextDocument.Version, DateTime.Now)) + context.Emit(SolutionChange updatedSolution) + context.Emit(OpenDocAdd(changeParams.TextDocument.Uri, changeParams.TextDocument.Version, DateTime.Now)) - return Ok() + return Ok() } let didClose (context: ServerRequestContext) (closeParams: DidCloseTextDocumentParams) : Async> = - context.Emit(OpenDocRemove closeParams.TextDocument.Uri) - Ok() |> async.Return + if closeParams.TextDocument.Uri.EndsWith(".cshtml") then + Ok() |> async.Return + else + context.Emit(OpenDocRemove closeParams.TextDocument.Uri) + Ok() |> async.Return let willSave (_context: ServerRequestContext) (_p: WillSaveTextDocumentParams) : Async> = async { return Ok() @@ -122,22 +210,25 @@ module TextDocumentSync = async { return LspResult.notImplemented } let didSave (context: ServerRequestContext) (saveParams: DidSaveTextDocumentParams) : Async> = - // we need to add this file to solution if not already - let doc = context.GetDocument saveParams.TextDocument.Uri + if saveParams.TextDocument.Uri.EndsWith(".cshtml") then + Ok() |> async.Return + else + // we need to add this file to solution if not already + let doc = context.GetDocument saveParams.TextDocument.Uri - match doc with - | Some _ -> Ok() |> async.Return + match doc with + | Some _ -> Ok() |> async.Return - | None -> async { - let docFilePath = Util.parseFileUri saveParams.TextDocument.Uri - let! newDocMaybe = tryAddDocument logger docFilePath saveParams.Text.Value context.Solution + | None -> async { + let docFilePath = Util.parseFileUri saveParams.TextDocument.Uri + let! newDocMaybe = tryAddDocument logger context.Solution docFilePath saveParams.Text.Value - match newDocMaybe with - | Some newDoc -> - context.Emit(OpenDocTouch(saveParams.TextDocument.Uri, DateTime.Now)) - context.Emit(SolutionChange newDoc.Project.Solution) + match newDocMaybe with + | Some newDoc -> + context.Emit(OpenDocTouch(saveParams.TextDocument.Uri, DateTime.Now)) + context.Emit(SolutionChange newDoc.Project.Solution) - | None -> () + | None -> () - return Ok() - } + return Ok() + } diff --git a/src/CSharpLanguageServer/Handlers/Workspace.fs b/src/CSharpLanguageServer/Handlers/Workspace.fs index a18a1aa1..47a27a37 100644 --- a/src/CSharpLanguageServer/Handlers/Workspace.fs +++ b/src/CSharpLanguageServer/Handlers/Workspace.fs @@ -61,7 +61,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 = tryAddDocument logger context.Solution docFilePath fileText match newDocMaybe with | Some newDoc -> context.Emit(SolutionChange newDoc.Project.Solution) diff --git a/src/CSharpLanguageServer/RoslynHelpers.fs b/src/CSharpLanguageServer/RoslynHelpers.fs index b84a0baf..fc8c7030 100644 --- a/src/CSharpLanguageServer/RoslynHelpers.fs +++ b/src/CSharpLanguageServer/RoslynHelpers.fs @@ -763,9 +763,9 @@ let getProjectForPathOnSolution (solution: Solution) (filePath: string) : Projec let tryAddDocument (logger: ILogger) + (solution: Solution) (docFilePath: string) (text: string) - (solution: Solution) : Async = async { let projectOnPath = getProjectForPathOnSolution solution docFilePath @@ -872,3 +872,44 @@ let initializeMSBuild (logger: ILogger) : unit = ) MSBuildLocator.RegisterInstance(vsInstance) + + +let getRazorDocumentForUri (solution: Solution) (uri: string) : Async<(Project * Compilation * SyntaxTree) option> = async { + let cshtmlPath = uri |> Uri.toPath + let cshtmlDirectory = Path.GetDirectoryName(cshtmlPath) + let normalizedTargetDir = Path.GetFullPath(cshtmlDirectory) + + 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 + )) + + let projectBaseDir = Path.GetDirectoryName(projectForPath.Value.FilePath) + + let! compilation = projectForPath.Value.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 -> (projectForPath.Value, compilation, cst)) +} diff --git a/src/CSharpLanguageServer/State/ServerRequestContext.fs b/src/CSharpLanguageServer/State/ServerRequestContext.fs index 72e15f7e..6c21a0d1 100644 --- a/src/CSharpLanguageServer/State/ServerRequestContext.fs +++ b/src/CSharpLanguageServer/State/ServerRequestContext.fs @@ -1,5 +1,8 @@ namespace CSharpLanguageServer.State +open System +open System.IO + open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.FindSymbols open Ionide.LanguageServerProtocol.Types @@ -33,7 +36,8 @@ type ServerRequestContext(requestId: int, state: ServerState, emitServerEvent) = ) | None -> async.Return() - member this.GetDocumentForUriOfType = getDocumentForUriOfType this.State + member this.GetDocumentForUriOfType docType uri = + getDocumentForUriOfType this.State docType uri member this.GetUserDocument(u: string) = this.GetDocumentForUriOfType UserDocument u |> Option.map fst @@ -134,15 +138,62 @@ type ServerRequestContext(requestId: int, state: ServerState, emitServerEvent) = return aggregatedLspLocations } - member this.FindSymbol' (uri: DocumentUri) (pos: Position) : Async<(ISymbol * Project * Document option) option> = async { - match this.GetDocument uri with + member this.GetSemanticModel(uri: DocumentUri) : Async = async { + match state.Solution with | None -> return None - | 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 - return symbol |> Option.ofObj |> Option.map (fun sym -> sym, doc.Project, Some doc) + | Some solution -> + if uri.EndsWith(".cshtml") then + match! getRazorDocumentForUri solution uri with + | None -> return None + | Some(_, compilation, cshtmlTree) -> return compilation.GetSemanticModel(cshtmlTree) |> Option.ofObj + else + match this.GetDocument uri with + | None -> return None + | Some doc -> + let! ct = Async.CancellationToken + let! semanticModel = doc.GetSemanticModelAsync(ct) |> Async.AwaitTask + return semanticModel |> Option.ofObj + } + + member this.FindSymbol' (uri: DocumentUri) (pos: Position) : Async<(ISymbol * Project * Document option) option> = async { + if uri.EndsWith(".cshtml") then + match! getRazorDocumentForUri state.Solution.Value uri with + | 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)) + + | None -> return None + else + match this.GetDocument uri with + | None -> return None + | 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 + return symbol |> Option.ofObj |> Option.map (fun sym -> sym, doc.Project, Some doc) } member this.FindSymbol (uri: DocumentUri) (pos: Position) : Async = diff --git a/src/CSharpLanguageServer/State/ServerState.fs b/src/CSharpLanguageServer/State/ServerState.fs index 19aaaf15..eead78bd 100644 --- a/src/CSharpLanguageServer/State/ServerState.fs +++ b/src/CSharpLanguageServer/State/ServerState.fs @@ -277,6 +277,106 @@ let processDumpAndResetRequestStats (logger: ILogger) state = LastStatsDumpTime = DateTime.Now } +let processPushDiagnosticsProcessPendingDocumentsEvent (logger: ILogger) state postSelf _msg : Async = async { + match state.PushDiagnosticsCurrentDocTask with + | Some _ -> + // another document is still being processed, do nothing + return state + + | None -> + // try pull next doc from the backlog to process + let nextDocUri, newBacklog = + match state.PushDiagnosticsDocumentBacklog with + | [] -> (None, []) + | uri :: remainder -> (Some uri, remainder) + + // push diagnostic is enabled only if pull diagnostics is + // not reported to be supported by the client + let diagnosticPullSupported = + state.ClientCapabilities.TextDocument + |> Option.map _.Diagnostic + |> Option.map _.IsSome + |> Option.defaultValue false + + match diagnosticPullSupported, nextDocUri with + | false, Some docUri -> + let newState = + { state with + PushDiagnosticsDocumentBacklog = newBacklog } + + let docAndTypeMaybe = docUri |> getDocumentForUriOfType state AnyDocument + + match docAndTypeMaybe with + | None -> + match! getRazorDocumentForUri state.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, _docType) -> + let resolveDocumentDiagnostics () : Task = task { + let! semanticModelMaybe = doc.GetSemanticModelAsync() + + match semanticModelMaybe |> Option.ofObj with + | None -> + Error(Exception("could not GetSemanticModelAsync")) + |> PushDiagnosticsDocumentDiagnosticsResolution + |> postSelf + + | Some semanticModel -> + let diagnostics = + semanticModel.GetDiagnostics() + |> Seq.map Diagnostic.fromRoslynDiagnostic + |> Seq.map fst + |> Array.ofSeq + + Ok(docUri, None, diagnostics) + |> PushDiagnosticsDocumentDiagnosticsResolution + |> postSelf + } + + let newTask = Task.Run(resolveDocumentDiagnostics) + + let newState = + { newState with + PushDiagnosticsCurrentDocTask = Some(docUri, newTask) } + + return newState + + | _, _ -> + // backlog is empty or pull diagnostics is enabled instead,--nothing to do + return state +} + + let processServerEvent (logger: ILogger) state postSelf msg : Async = async { match msg with | SettingsChange newSettings -> @@ -451,76 +551,7 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async PushDiagnosticsDocumentBacklog = newBacklog } | PushDiagnosticsProcessPendingDocuments -> - match state.PushDiagnosticsCurrentDocTask with - | Some _ -> - // another document is still being processed, do nothing - return state - | None -> - // try pull next doc from the backlog to process - let nextDocUri, newBacklog = - match state.PushDiagnosticsDocumentBacklog with - | [] -> (None, []) - | uri :: remainder -> (Some uri, remainder) - - // push diagnostic is enabled only if pull diagnostics is - // not reported to be supported by the client - let diagnosticPullSupported = - state.ClientCapabilities.TextDocument - |> Option.map _.Diagnostic - |> Option.map _.IsSome - |> Option.defaultValue false - - match diagnosticPullSupported, nextDocUri with - | false, Some docUri -> - let newState = - { state with - PushDiagnosticsDocumentBacklog = newBacklog } - - let docAndTypeMaybe = docUri |> getDocumentForUriOfType state AnyDocument - - match docAndTypeMaybe with - | 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, _docType) -> - let resolveDocumentDiagnostics () : Task = task { - let! semanticModelMaybe = doc.GetSemanticModelAsync() - - match semanticModelMaybe |> Option.ofObj with - | None -> - Error(Exception("could not GetSemanticModelAsync")) - |> PushDiagnosticsDocumentDiagnosticsResolution - |> postSelf - - | Some semanticModel -> - let diagnostics = - semanticModel.GetDiagnostics() - |> Seq.map Diagnostic.fromRoslynDiagnostic - |> Seq.map fst - |> Array.ofSeq - - Ok(docUri, None, diagnostics) - |> PushDiagnosticsDocumentDiagnosticsResolution - |> postSelf - } - - let newTask = Task.Run(resolveDocumentDiagnostics) - - let newState = - { newState with - PushDiagnosticsCurrentDocTask = Some(docUri, newTask) } - - return newState - - | _, _ -> - // backlog is empty or pull diagnostics is enabled instead,--nothing to do - return state + return! processPushDiagnosticsProcessPendingDocumentsEvent logger state postSelf msg | PushDiagnosticsDocumentDiagnosticsResolution result -> // enqueue processing for the next doc on the queue (if any) diff --git a/src/CSharpLanguageServer/Types.fs b/src/CSharpLanguageServer/Types.fs index 28e1fd62..109786c3 100644 --- a/src/CSharpLanguageServer/Types.fs +++ b/src/CSharpLanguageServer/Types.fs @@ -50,7 +50,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 97bf2ef4..e8504224 100644 --- a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj +++ b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj @@ -11,18 +11,19 @@ + + + + + - - - - - + - + diff --git a/tests/CSharpLanguageServer.Tests/CodeActionTests.fs b/tests/CSharpLanguageServer.Tests/CodeActionTests.fs index 42de0f0c..fc2ed18c 100644 --- a/tests/CSharpLanguageServer.Tests/CodeActionTests.fs +++ b/tests/CSharpLanguageServer.Tests/CodeActionTests.fs @@ -124,5 +124,4 @@ let ``extract interface code action should extract an interface`` () = Assert.AreEqual(expectedImplementInterfaceEdits, implementEdits |> TextEdit.normalizeNewText) | _ -> failwith "Expected exactly one U2.C1 edit in both create/implement" - | _ -> failwith "Unexpected edit structure" diff --git a/tests/CSharpLanguageServer.Tests/CompletionTests.fs b/tests/CSharpLanguageServer.Tests/CompletionTests.fs index 24a65f77..1efc0ce9 100644 --- a/tests/CSharpLanguageServer.Tests/CompletionTests.fs +++ b/tests/CSharpLanguageServer.Tests/CompletionTests.fs @@ -136,3 +136,85 @@ let ``completion works for extension methods`` () = Assert.IsFalse(itemResolved.Documentation.IsSome) | _ -> 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 new file mode 100644 index 00000000..259d30b6 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/DocumentHighlightTests.fs @@ -0,0 +1,59 @@ +module CSharpLanguageServer.Tests.DocumentHighlightTests + +open System + +open NUnit.Framework +open Ionide.LanguageServerProtocol.Types +open Ionide.LanguageServerProtocol.Server + +open CSharpLanguageServer.Tests.Tooling + +[] +let ``test textDocument/documentHighlight works in .cs file`` () = + use client = activateFixture "genericProject" + use classFile = client.Open("Project/Class.cs") + + let highlightParams: DocumentHighlightParams = + { TextDocument = { Uri = classFile.Uri } + Position = { Line = 9u; Character = 8u } + WorkDoneToken = None + PartialResultToken = None } + + let highlights: DocumentHighlight[] option = + client.Request("textDocument/documentHighlight", highlightParams) + + let expectedHighlights: DocumentHighlight list = + [ { Range = + { Start = { Line = 2u; Character = 16u } + End = { Line = 2u; Character = 23u } } + Kind = Some DocumentHighlightKind.Read } + + { Range = + { Start = { Line = 9u; Character = 8u } + End = { Line = 9u; Character = 15u } } + 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..519314c2 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/ClassWithExtensionMethods.cs @@ -0,0 +1,23 @@ +<<<<<<<< HEAD:tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/ClassForCompletionTestsWithExtensionMethods.cs +class ClassForCompletionWithExtensionMethods +======== +class ClassWithExtensionMethods +>>>>>>>> 902dbd5 (squash):tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/ClassWithExtensionMethods.cs +{ + public void MethodA(string arg) + { + this. + } +} + +public static class ClassExtensions +{ +<<<<<<<< HEAD:tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/ClassForCompletionTestsWithExtensionMethods.cs + public static string MethodB(this ClassForCompletionWithExtensionMethods input) +======== + public static string MethodB(this ClassWithExtensionMethods input) +>>>>>>>> 902dbd5 (squash):tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/ClassWithExtensionMethods.cs + { + return "ok"; + } +} diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Controllers/TestController.cs b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Controllers/TestController.cs new file mode 100644 index 00000000..e4f76f59 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Controllers/TestController.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; +using Project.Models.Test; + +namespace Printlog.Web.ClientPart.Controllers; + +public class TestController : Controller +{ + public IActionResult Index() + { + var model = new IndexViewModel() + { + Output = "test" + }; + + return View(model); + } +} diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Models/Test/IndexViewModel.cs b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Models/Test/IndexViewModel.cs new file mode 100644 index 00000000..02f6157a --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Models/Test/IndexViewModel.cs @@ -0,0 +1,5 @@ +namespace Project.Models.Test; +public class IndexViewModel +{ + public string? Output { get; set; } +} diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Program.cs b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Program.cs new file mode 100644 index 00000000..698f461f --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; + +namespace Project; + +public class Program +{ + public static async Task Main(string[] args) + { + await BuildWebHost(args).RunAsync(); + } + + public static IWebHost BuildWebHost(string[] args) + { + var builder = WebHost.CreateDefaultBuilder(args); + builder = builder.UseKestrel().UseStartup(); + return builder.Build(); + } +} diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Project.csproj b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Project.csproj index 29aeae9e..9d715580 100644 --- a/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Project.csproj +++ b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Project.csproj @@ -1,6 +1,10 @@ - - - Exe - net9.0 - + + + net9.0 + enable + + + + + diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Startup.cs b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Startup.cs new file mode 100644 index 00000000..d089977c --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Startup.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Project; + +public class Startup +{ + public Startup(IConfiguration configuration, IWebHostEnvironment env) + { + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddOptions(); + } + + public void Configure( + IApplicationBuilder app, + IWebHostEnvironment env) + { + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { + endpoints.MapControllers(); + }); + } +} diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Views/Test/Index.cshtml b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Views/Test/Index.cshtml new file mode 100644 index 00000000..5182deb0 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Views/Test/Index.cshtml @@ -0,0 +1,8 @@ +@model Project.Models.Test.IndexViewModel +@(Model.) +@Model. +@Model.Output. +@{ + int x = 1; + x. +} diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Views/_ViewImports.cshtml b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Views/_ViewImports.cshtml new file mode 100644 index 00000000..a757b413 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Views/_ViewImports.cshtml @@ -0,0 +1 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Views/_ViewStart.cshtml b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Views/_ViewStart.cshtml new file mode 100644 index 00000000..a5f10045 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/tests/CSharpLanguageServer.Tests/HoverTests.fs b/tests/CSharpLanguageServer.Tests/HoverTests.fs index f1ce31eb..7ca585e3 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,25 @@ 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 7bab5060..a255b4c6 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 indexCshtmlFile = client.Open("Project/Views/Test/Index.cshtml") + + use completionTestsCshtmlFile = + client.Open("Project/Views/Test/CompletionTests.cshtml") let referenceParams0: ReferenceParams = { TextDocument = { Uri = testIndexViewModelCsFile.Uri } @@ -162,7 +164,7 @@ let testReferenceWorksToAspNetRazorPageReferencedValue () = client.Request("textDocument/references", referenceParams0) Assert.IsTrue(locations0.IsSome) - Assert.AreEqual(2, locations0.Value.Length) + Assert.AreEqual(3, locations0.Value.Length) let expectedLocations0: Location array = [| { Uri = testControllerCsFile.Uri @@ -170,12 +172,17 @@ let testReferenceWorksToAspNetRazorPageReferencedValue () = { Start = { Line = 11u; Character = 12u } End = { Line = 11u; Character = 18u } } } - { Uri = viewsTestIndexCshtmlFile.Uri + { 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 } } } |] - Assert.AreEqual(expectedLocations0, locations0.Value) + Assert.AreEqual(expectedLocations0, locations0.Value |> Array.sortBy _.Uri) // // do same but with IncludeDeclaration=true @@ -191,14 +198,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 } @@ -224,3 +236,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/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Controllers/TestController.cs b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Controllers/TestController.cs new file mode 100644 index 00000000..e4f76f59 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Controllers/TestController.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; +using Project.Models.Test; + +namespace Printlog.Web.ClientPart.Controllers; + +public class TestController : Controller +{ + public IActionResult Index() + { + var model = new IndexViewModel() + { + Output = "test" + }; + + return View(model); + } +} diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Models/Test/IndexViewModel.cs b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Models/Test/IndexViewModel.cs new file mode 100644 index 00000000..02f6157a --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Models/Test/IndexViewModel.cs @@ -0,0 +1,5 @@ +namespace Project.Models.Test; +public class IndexViewModel +{ + public string? Output { get; set; } +} diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Program.cs b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Program.cs new file mode 100644 index 00000000..698f461f --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; + +namespace Project; + +public class Program +{ + public static async Task Main(string[] args) + { + await BuildWebHost(args).RunAsync(); + } + + public static IWebHost BuildWebHost(string[] args) + { + var builder = WebHost.CreateDefaultBuilder(args); + builder = builder.UseKestrel().UseStartup(); + return builder.Build(); + } +} diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Project.csproj b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Project.csproj new file mode 100644 index 00000000..9d715580 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Project.csproj @@ -0,0 +1,10 @@ + + + net9.0 + enable + + + + + + diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Startup.cs b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Startup.cs new file mode 100644 index 00000000..d089977c --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Startup.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Project; + +public class Startup +{ + public Startup(IConfiguration configuration, IWebHostEnvironment env) + { + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddOptions(); + } + + public void Configure( + IApplicationBuilder app, + IWebHostEnvironment env) + { + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { + endpoints.MapControllers(); + }); + } +} diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Views/Test/Index.cshtml b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Views/Test/Index.cshtml new file mode 100644 index 00000000..b094abcb --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Views/Test/Index.cshtml @@ -0,0 +1,2 @@ +@model Project.Models.Test.IndexViewModel +@Model.Output diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Views/_ViewImports.cshtml b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Views/_ViewImports.cshtml new file mode 100644 index 00000000..a757b413 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Views/_ViewImports.cshtml @@ -0,0 +1 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Views/_ViewStart.cshtml b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Views/_ViewStart.cshtml new file mode 100644 index 00000000..a5f10045 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksFromRazorPageReferencedValue/Project/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Controllers/TestController.cs b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Controllers/TestController.cs new file mode 100644 index 00000000..e4f76f59 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Controllers/TestController.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Mvc; +using Project.Models.Test; + +namespace Printlog.Web.ClientPart.Controllers; + +public class TestController : Controller +{ + public IActionResult Index() + { + var model = new IndexViewModel() + { + Output = "test" + }; + + return View(model); + } +} diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Models/Test/IndexViewModel.cs b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Models/Test/IndexViewModel.cs new file mode 100644 index 00000000..02f6157a --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Models/Test/IndexViewModel.cs @@ -0,0 +1,5 @@ +namespace Project.Models.Test; +public class IndexViewModel +{ + public string? Output { get; set; } +} diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Program.cs b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Program.cs new file mode 100644 index 00000000..698f461f --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; + +namespace Project; + +public class Program +{ + public static async Task Main(string[] args) + { + await BuildWebHost(args).RunAsync(); + } + + public static IWebHost BuildWebHost(string[] args) + { + var builder = WebHost.CreateDefaultBuilder(args); + builder = builder.UseKestrel().UseStartup(); + return builder.Build(); + } +} diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Project.csproj b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Project.csproj new file mode 100644 index 00000000..9d715580 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Project.csproj @@ -0,0 +1,10 @@ + + + net9.0 + enable + + + + + + diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Startup.cs b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Startup.cs new file mode 100644 index 00000000..d089977c --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Startup.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Project; + +public class Startup +{ + public Startup(IConfiguration configuration, IWebHostEnvironment env) + { + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddOptions(); + } + + public void Configure( + IApplicationBuilder app, + IWebHostEnvironment env) + { + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => { + endpoints.MapControllers(); + }); + } +} diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Views/Test/Index.cshtml b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Views/Test/Index.cshtml new file mode 100644 index 00000000..b094abcb --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Views/Test/Index.cshtml @@ -0,0 +1,2 @@ +@model Project.Models.Test.IndexViewModel +@Model.Output diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Views/_ViewImports.cshtml b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Views/_ViewImports.cshtml new file mode 100644 index 00000000..a757b413 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Views/_ViewImports.cshtml @@ -0,0 +1 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Views/_ViewStart.cshtml b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Views/_ViewStart.cshtml new file mode 100644 index 00000000..a5f10045 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/TestData/testReferenceWorksToRazorPageReferencedValue/Project/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/tests/CSharpLanguageServer.Tests/WorkspaceSymbolTests.fs b/tests/CSharpLanguageServer.Tests/WorkspaceSymbolTests.fs index 8e1c1700..5722a2f3 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(5, sis.Length) let sym0 = sis[0] Assert.AreEqual("Class", sym0.Name)