diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cafbc20..7a319ce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Update Microsoft.Build.Locator to 1.10.12 - By @razzmatazz in https://github.com/razzmatazz/csharp-language-server/pull/296 * Add experimental support for Razor (.cshtml) files. "razor-support" feature needs to be enabled via command line - - By @razzmatazz in https://github.com/razzmatazz/csharp-language-server/pull/273 + - By @razzmatazz in https://github.com/razzmatazz/csharp-language-server/pull/273, https://github.com/razzmatazz/csharp-language-server/pull/304, https://github.com/razzmatazz/csharp-language-server/pull/310 * Add suggestion to install specific dotnet sdk version - By @jhamm in https://github.com/razzmatazz/csharp-language-server/pull/299 - Reported by @pandasoli in https://github.com/razzmatazz/csharp-language-server/issues/215 diff --git a/src/CSharpLanguageServer/Handlers/Completion.fs b/src/CSharpLanguageServer/Handlers/Completion.fs index 3b010086..0c255fd7 100644 --- a/src/CSharpLanguageServer/Handlers/Completion.fs +++ b/src/CSharpLanguageServer/Handlers/Completion.fs @@ -263,24 +263,7 @@ module Completion = //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) + let positionAndToken = solutionSemanticModelMappedPositionAndToken cshtmlTree p.Position match positionAndToken with | None -> return None diff --git a/src/CSharpLanguageServer/Handlers/Rename.fs b/src/CSharpLanguageServer/Handlers/Rename.fs index ec9b37ea..83a1ad4f 100644 --- a/src/CSharpLanguageServer/Handlers/Rename.fs +++ b/src/CSharpLanguageServer/Handlers/Rename.fs @@ -15,6 +15,7 @@ open Microsoft.Extensions.Logging open CSharpLanguageServer.State open CSharpLanguageServer.Logging open CSharpLanguageServer.Roslyn.Conversions +open CSharpLanguageServer.Roslyn.Solution open CSharpLanguageServer.Util open CSharpLanguageServer.Lsp.Workspace open CSharpLanguageServer.Lsp.WorkspaceFolder @@ -113,19 +114,28 @@ module Rename = RegisterOptions = registerOptions |> serialize |> Some } let prepare (context: ServerRequestContext) (p: PrepareRenameParams) : AsyncLspResult = async { + let! wf, semModel = + p.TextDocument.Uri |> workspaceDocumentSemanticModel context.Workspace - let wf, docForUri = - p.TextDocument.Uri |> workspaceDocument context.Workspace UserDocument - - match docForUri with - | None -> return None |> LspResult.success - | Some doc -> + match wf, semModel with + | Some wf, Some semModel -> let! ct = Async.CancellationToken - let! docSyntaxTree = doc.GetSyntaxTreeAsync(ct) |> Async.AwaitTask - let! docText = doc.GetTextAsync(ct) |> Async.AwaitTask + let positionAndToken = + solutionSemanticModelMappedPositionAndToken semModel.SyntaxTree p.Position + + let (position, token) = positionAndToken.Value + + let symbol = + token + |> Option.bind (fun x -> x.Parent |> Option.ofObj) + |> Option.map (fun parentToken -> semModel.GetSymbolInfo(parentToken)) + |> Option.bind (fun x -> x.Symbol |> Option.ofObj) + + let! docText = semModel.SyntaxTree.GetTextAsync(ct) |> Async.AwaitTask let position = Position.toRoslynPosition docText.Lines p.Position - let! symbolMaybe = SymbolFinder.FindSymbolAtPositionAsync(doc, position, ct) |> Async.AwaitTask + + let! symbolMaybe = SymbolFinder.FindSymbolAtPositionAsync(semModel, position, wf.Solution.Value.Workspace, ct) |> Async.AwaitTask let symbolIsFromMetadata = symbolMaybe @@ -138,7 +148,7 @@ module Rename = let textSpan = docText.Lines.GetTextSpan(linePositionSpan) - let! rootNode = docSyntaxTree.GetRootAsync(ct) |> Async.AwaitTask + let! rootNode = semModel.SyntaxTree.GetRootAsync(ct) |> Async.AwaitTask let nodeOnPos = rootNode.FindNode(textSpan, findInsideTrivia = false, getInnermostNodeForTie = true) @@ -170,6 +180,8 @@ module Rename = | _, _ -> None return rangeWithPlaceholderMaybe |> LspResult.success + + | _, _ -> return None |> LspResult.success } let handle (context: ServerRequestContext) (p: RenameParams) : AsyncLspResult = async { diff --git a/src/CSharpLanguageServer/Roslyn/Solution.fs b/src/CSharpLanguageServer/Roslyn/Solution.fs index 7c78e9eb..5f81a453 100644 --- a/src/CSharpLanguageServer/Roslyn/Solution.fs +++ b/src/CSharpLanguageServer/Roslyn/Solution.fs @@ -460,3 +460,23 @@ let solutionFindSymbolForRazorDocumentPath solution cshtmlPath pos = async { return symbol |> Option.map (fun sym -> (sym, project, None)) } + +let solutionSemanticModelMappedPositionAndToken (cshtmlTree: SyntaxTree) pos = + 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) + + positionAndToken diff --git a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj index 58a345fc..abc64876 100644 --- a/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj +++ b/tests/CSharpLanguageServer.Tests/CSharpLanguageServer.Tests.fsproj @@ -10,6 +10,8 @@ + + diff --git a/tests/CSharpLanguageServer.Tests/CSharpMetadataTests.fs b/tests/CSharpLanguageServer.Tests/CSharpMetadataTests.fs index f089a09e..4c5c2ced 100644 --- a/tests/CSharpLanguageServer.Tests/CSharpMetadataTests.fs +++ b/tests/CSharpLanguageServer.Tests/CSharpMetadataTests.fs @@ -15,7 +15,7 @@ let ``test csharp/metadata works`` () = let typeDefinitionParams0: TypeDefinitionParams = { TextDocument = { Uri = classFile.Uri } - Position = { Line = 9u; Character = 16u } + Position = { Line = 10u; Character = 16u } WorkDoneToken = None PartialResultToken = None } diff --git a/tests/CSharpLanguageServer.Tests/DocumentHighlightTests.fs b/tests/CSharpLanguageServer.Tests/DocumentHighlightTests.fs index 16e5221f..2883b062 100644 --- a/tests/CSharpLanguageServer.Tests/DocumentHighlightTests.fs +++ b/tests/CSharpLanguageServer.Tests/DocumentHighlightTests.fs @@ -15,7 +15,7 @@ let ``test textDocument/documentHighlight works in .cs file`` () = let highlightParams: DocumentHighlightParams = { TextDocument = { Uri = classFile.Uri } - Position = { Line = 9u; Character = 8u } + Position = { Line = 10u; Character = 8u } WorkDoneToken = None PartialResultToken = None } @@ -29,8 +29,8 @@ let ``test textDocument/documentHighlight works in .cs file`` () = Kind = Some DocumentHighlightKind.Read } { Range = - { Start = { Line = 9u; Character = 8u } - End = { Line = 9u; Character = 15u } } + { Start = { Line = 10u; Character = 8u } + End = { Line = 10u; Character = 15u } } Kind = Some DocumentHighlightKind.Read } ] Assert.AreEqual(Some expectedHighlights, highlights |> Option.map List.ofArray) diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/aspnetProject/Project/Views/Test/Index.cshtml b/tests/CSharpLanguageServer.Tests/Fixtures/aspnetProject/Project/Views/Test/Index.cshtml index b094abcb..1c315a88 100644 --- a/tests/CSharpLanguageServer.Tests/Fixtures/aspnetProject/Project/Views/Test/Index.cshtml +++ b/tests/CSharpLanguageServer.Tests/Fixtures/aspnetProject/Project/Views/Test/Index.cshtml @@ -1,2 +1,6 @@ @model Project.Models.Test.IndexViewModel @Model.Output +@{ + int x = 1; + x = 2; +} diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Class.cs b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Class.cs index 4097b2c8..3db5a17d 100644 --- a/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Class.cs +++ b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Class.cs @@ -3,6 +3,7 @@ class Class public void MethodA(string arg) { string str = ""; + Console.WriteLine(str); } public void MethodB(string arg) diff --git a/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Class.cs.str-renamed-to-xxx.txt b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Class.cs.str-renamed-to-xxx.txt new file mode 100644 index 00000000..299d95b5 --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/Fixtures/genericProject/Project/Class.cs.str-renamed-to-xxx.txt @@ -0,0 +1,13 @@ +class Class +{ + public void MethodA(string arg) + { + string xxx = ""; + Console.WriteLine(xxx); + } + + public void MethodB(string arg) + { + MethodA(arg); + } +} diff --git a/tests/CSharpLanguageServer.Tests/ReferenceTests.fs b/tests/CSharpLanguageServer.Tests/ReferenceTests.fs index a52e8190..9d80a58f 100644 --- a/tests/CSharpLanguageServer.Tests/ReferenceTests.fs +++ b/tests/CSharpLanguageServer.Tests/ReferenceTests.fs @@ -41,8 +41,8 @@ let testReferenceWorks () = let expectedLocations1: Location array = [| { Uri = classFile.Uri Range = - { Start = { Line = 9u; Character = 8u } - End = { Line = 9u; Character = 15u } } } |] + { Start = { Line = 10u; Character = 8u } + End = { Line = 10u; Character = 15u } } } |] Assert.AreEqual(expectedLocations1, locations1.Value) @@ -68,8 +68,8 @@ let testReferenceWorks () = { Uri = classFile.Uri Range = - { Start = { Line = 9u; Character = 8u } - End = { Line = 9u; Character = 15u } } } |] + { Start = { Line = 10u; Character = 8u } + End = { Line = 10u; Character = 15u } } } |] Assert.AreEqual(expectedLocations2, locations2.Value) diff --git a/tests/CSharpLanguageServer.Tests/RenameTests.fs b/tests/CSharpLanguageServer.Tests/RenameTests.fs new file mode 100644 index 00000000..80f3488a --- /dev/null +++ b/tests/CSharpLanguageServer.Tests/RenameTests.fs @@ -0,0 +1,107 @@ +module CSharpLanguageServer.Tests.RenameTests + +open System +open System.IO + +open NUnit.Framework +open Ionide.LanguageServerProtocol.Types + +open CSharpLanguageServer.Tests.Tooling + +[] +let ``rename can be applied to a variable`` () = + use client = activateFixture "genericProject" + + use classFile = client.Open "Project/Class.cs" + + let prepareParams: PrepareRenameParams = + { TextDocument = { Uri = classFile.Uri } + Position = { Line = 4u; Character = 15u } + WorkDoneToken = None } + + let prepareResult: option = + client.Request("textDocument/prepareRename", prepareParams) + + let expectedPrepareResult: PrepareRenameResult = + { Range = + { Start = { Line = 4u; Character = 15u } + End = { Line = 4u; Character = 18u } } + Placeholder = "str" } + |> U3.C2 + + Assert.AreEqual(Some expectedPrepareResult, prepareResult) + + let renameParams: RenameParams = + { TextDocument = { Uri = classFile.Uri } + Position = { Line = 4u; Character = 15u } + WorkDoneToken = None + NewName = "xxx" } + + let renameResult: option = + client.Request("textDocument/rename", renameParams) + + match renameResult with + | None -> failwith "Some WorkspaceEdit was expected" + + | Some workspaceEdit -> + let textEdits = workspaceEdit.Changes.Value |> Map.find classFile.Uri + + let expectedClassContents = + File + .ReadAllText(Path.Combine(client.SolutionDir, "Project", "Class.cs.str-renamed-to-xxx.txt")) + .ReplaceLineEndings("\n") + + let actualClassContents = + classFile.GetFileContentsWithTextEditsApplied(textEdits).ReplaceLineEndings("\n") + + Assert.AreEqual(expectedClassContents, actualClassContents) + +[] +let ``rename can be applied to a variable in .cshtml file`` () = + use client = activateFixture "aspnetProject" + + use indexCshtmlFile = client.Open "Project/Views/Test/Index.cshtml" + + let prepareParams: PrepareRenameParams = + { TextDocument = { Uri = indexCshtmlFile.Uri } + Position = { Line = 3u; Character = 11u } + WorkDoneToken = None } + + let prepareResult: option = + client.Request("textDocument/prepareRename", prepareParams) + + let expectedPrepareResult: PrepareRenameResult = + { Range = + { Start = { Line = 3u; Character = 11u } + End = { Line = 3u; Character = 11u } } + Placeholder = "x" } + |> U3.C2 + + Assert.AreEqual(Some expectedPrepareResult, prepareResult) + +(* + let renameParams: RenameParams = + { TextDocument = { Uri = classFile.Uri } + Position = { Line = 4u; Character = 15u } + WorkDoneToken = None + NewName = "xxx" } + + let renameResult: option = + client.Request("textDocument/rename", renameParams) + + match renameResult with + | None -> failwith "Some WorkspaceEdit was expected" + + | Some workspaceEdit -> + let textEdits = workspaceEdit.Changes.Value |> Map.find classFile.Uri + + let expectedClassContents = + File + .ReadAllText(Path.Combine(client.SolutionDir, "Project", "Class.cs.str-renamed-to-xxx.txt")) + .ReplaceLineEndings("\n") + + let actualClassContents = + classFile.GetFileContentsWithTextEditsApplied(textEdits).ReplaceLineEndings("\n") + + Assert.AreEqual(expectedClassContents, actualClassContents) +*) diff --git a/tests/CSharpLanguageServer.Tests/SignatureHelpTests.fs b/tests/CSharpLanguageServer.Tests/SignatureHelpTests.fs index 84cf0583..e0b5e03f 100644 --- a/tests/CSharpLanguageServer.Tests/SignatureHelpTests.fs +++ b/tests/CSharpLanguageServer.Tests/SignatureHelpTests.fs @@ -12,7 +12,7 @@ let ``test textDocument/signatureHelp works`` () = let signatureHelpParams0: SignatureHelpParams = { TextDocument = { Uri = classFile.Uri } - Position = { Line = 9u; Character = 16u } + Position = { Line = 10u; Character = 16u } WorkDoneToken = None Context = None } diff --git a/tests/CSharpLanguageServer.Tests/TypeDefinitionTests.fs b/tests/CSharpLanguageServer.Tests/TypeDefinitionTests.fs index 2a75acd7..e08b3d9d 100644 --- a/tests/CSharpLanguageServer.Tests/TypeDefinitionTests.fs +++ b/tests/CSharpLanguageServer.Tests/TypeDefinitionTests.fs @@ -14,7 +14,7 @@ let ``test textDocument/typeDefinition works`` () = let typeDefinitionParams0: TypeDefinitionParams = { TextDocument = { Uri = classFile.Uri } - Position = { Line = 9u; Character = 16u } + Position = { Line = 10u; Character = 16u } WorkDoneToken = None PartialResultToken = None }