提交 84d6a7a5 编写于 作者: D David Barbet

Add tests and fix impl

上级 40c74504
......@@ -18,7 +18,6 @@
using Microsoft.CodeAnalysis.LanguageServer.Handler.Commands;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.UnitTests;
using Microsoft.VisualStudio.Composition;
using Microsoft.VisualStudio.Text.Adornments;
using Newtonsoft.Json;
......@@ -192,26 +191,46 @@ protected Workspace CreateTestWorkspace(string[] markups, out Dictionary<string,
{
var workspace = TestWorkspace.CreateCSharp(markups, exportProvider: GetExportProvider());
var solution = workspace.CurrentSolution;
locations = new Dictionary<string, IList<LSP.Location>>();
foreach (var document in workspace.Documents)
{
var text = solution.GetDocument(document.Id).GetTextSynchronously(CancellationToken.None);
foreach (var (name, spans) in document.AnnotatedSpans)
{
locations
.GetOrAdd(name, _ => new List<LSP.Location>())
.AddRange(spans.Select(span => ProtocolConversions.TextSpanToLocation(span, text, new Uri(GetDocumentFilePathFromName(document.Name)))));
}
solution = solution.WithDocumentFilePath(document.Id, GetDocumentFilePathFromName(document.Name));
}
workspace.ChangeSolution(solution);
locations = GetAnnotatedLocations(workspace, solution);
return workspace;
}
protected Workspace CreateXmlTestWorkspace(string xmlContent, out Dictionary<string, IList<LSP.Location>> locations)
{
var workspace = TestWorkspace.Create(xmlContent, exportProvider: GetExportProvider());
locations = GetAnnotatedLocations(workspace, workspace.CurrentSolution);
return workspace;
}
private Dictionary<string, IList<LSP.Location>> GetAnnotatedLocations(TestWorkspace workspace, Solution solution)
{
var locations = new Dictionary<string, IList<LSP.Location>>();
foreach (var testDocument in workspace.Documents)
{
var document = solution.GetDocument(testDocument.Id);
var text = document.GetTextSynchronously(CancellationToken.None);
foreach (var (name, spans) in testDocument.AnnotatedSpans)
{
var locationsForName = locations.GetOrValue(name, new List<LSP.Location>());
locationsForName.AddRange(spans.Select(span => ProtocolConversions.TextSpanToLocation(span, text, new Uri(document.FilePath))));
// Linked files will return duplicate annotated Locations for each document that links to the same file.
// Since the test output only cares about the actual file, make sure we de-dupe before returning.
locations[name] = locationsForName.Distinct().ToList();
}
}
return locations;
}
// Private protected because LanguageServerProtocol is internal
private protected static LanguageServerProtocol GetLanguageServer(Solution solution)
{
......
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer
{
internal class TextEditEqualityComparer : IEqualityComparer<TextEdit>
{
public bool Equals(TextEdit x, TextEdit y)
{
return EqualityComparer<Range>.Default.Equals(x.Range, y.Range) &&
x.NewText == y.NewText;
}
public int GetHashCode(TextEdit obj)
{
var hashCode = -1114201889;
hashCode = (hashCode * -1521134295) + EqualityComparer<Range>.Default.GetHashCode(obj.Range);
hashCode = (hashCode * -1521134295) + EqualityComparer<string>.Default.GetHashCode(obj.NewText);
return hashCode;
}
}
}
......@@ -5,36 +5,38 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Roslyn.Utilities;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler
{
[ExportLspMethod(LSP.Methods.TextDocumentRenameName), Shared]
internal class RenameHandler : IRequestHandler<LSP.RenameParams, WorkspaceEdit>
internal class RenameHandler : IRequestHandler<LSP.RenameParams, WorkspaceEdit?>
{
private static TextEditEqualityComparer s_textEditEqualityComparer = new TextEditEqualityComparer();
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public RenameHandler()
{
}
public async Task<WorkspaceEdit> HandleRequestAsync(Solution solution, RenameParams request, ClientCapabilities clientCapabilities, CancellationToken cancellationToken)
public async Task<WorkspaceEdit?> HandleRequestAsync(Solution solution, RenameParams request, ClientCapabilities clientCapabilities, CancellationToken cancellationToken)
{
WorkspaceEdit workspaceEdit = null;
WorkspaceEdit? workspaceEdit = null;
var document = solution.GetDocumentFromURI(request.TextDocument.Uri);
if (document != null)
{
var renameService = document.Project.LanguageServices.GetService<IEditorInlineRenameService>();
var renameService = document.Project.LanguageServices.GetRequiredService<IEditorInlineRenameService>();
var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false);
var renameInfo = await renameService.GetRenameInfoAsync(document, position, cancellationToken).ConfigureAwait(false);
......@@ -49,35 +51,40 @@ public async Task<WorkspaceEdit> HandleRequestAsync(Solution solution, RenamePar
var newSolution = renameReplacementInfo.NewSolution;
var solutionChanges = newSolution.GetChanges(solution);
// Merge changes in linked files. Will result in linked file documents having the same changes in all linked documents.
// Once the changes are the same across linked files, we can take the distinct changes by file uri
var solutionWithLinkedFileChangesMerged = newSolution.WithMergedLinkedFileChangesAsync(solution, solutionChanges, cancellationToken: cancellationToken).Result;
solutionChanges = solutionWithLinkedFileChangesMerged.GetChanges(solution);
var changedDocuments = solutionChanges
.GetProjectChanges()
.SelectMany(p => p.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true))
.GroupBy(docId => newSolution.GetRequiredDocument(docId).FilePath).Select(group => group.First());
var changedDocuments = solutionWithLinkedFileChangesMerged.GetChanges(solution).GetProjectChanges().SelectMany(p => p.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true));
var documentEdits = new ArrayBuilder<TextDocumentEdit>();
foreach (var docId in changedDocuments)
// Linked files will create multiple documents with duplicate edits for the same actual file.
// So take only the unique edits per URI to avoid returning duplicate edits.
var textEdits = new Dictionary<Uri, List<TextEdit>>();
foreach (var changedDocument in changedDocuments)
{
var oldDoc = solution.GetRequiredDocument(docId);
var newDoc = newSolution.GetRequiredDocument(docId);
var textChanges = await newDoc.GetTextChangesAsync(oldDoc, cancellationToken).ConfigureAwait(false);
var oldText = await oldDoc.GetTextAsync(cancellationToken).ConfigureAwait(false);
var textDocumentEdit = new TextDocumentEdit
{
TextDocument = new VersionedTextDocumentIdentifier { Uri = newDoc.GetURI() },
Edits = textChanges.Select(tc => ProtocolConversions.TextChangeToTextEdit(tc, oldText)).ToArray()
};
documentEdits.Add(textDocumentEdit);
var documentUri = newSolution.GetRequiredDocument(changedDocument).GetURI();
var textChangesForUri = textEdits.GetOrValue(documentUri, new List<TextEdit>());
textChangesForUri.AddRange(await GetTextChangesAsync(changedDocument, newSolution, solution, cancellationToken).ConfigureAwait(false));
textEdits[documentUri] = textChangesForUri.Distinct(s_textEditEqualityComparer).ToList();
}
workspaceEdit = new WorkspaceEdit { DocumentChanges = documentEdits.ToArrayAndFree() };
var documentEdits = textEdits.Select(kvp => new TextDocumentEdit
{
TextDocument = new VersionedTextDocumentIdentifier { Uri = kvp.Key },
Edits = kvp.Value.ToArray()
}).ToArray();
workspaceEdit = new WorkspaceEdit { DocumentChanges = documentEdits };
}
return workspaceEdit;
static async Task<IEnumerable<TextEdit>> GetTextChangesAsync(DocumentId documentId, Solution newSolution, Solution oldSolution, CancellationToken cancellationToken)
{
var oldDoc = oldSolution.GetRequiredDocument(documentId);
var newDoc = newSolution.GetRequiredDocument(documentId);
var textChanges = await newDoc.GetTextChangesAsync(oldDoc, cancellationToken).ConfigureAwait(false);
var oldText = await oldDoc.GetTextAsync(cancellationToken).ConfigureAwait(false);
return textChanges.Select(textChange => ProtocolConversions.TextChangeToTextEdit(textChange, oldText));
}
}
}
}
......@@ -9,7 +9,7 @@
using Roslyn.Test.Utilities;
using LSP = Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Definitions
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Rename
{
public class RenameTests : AbstractLanguageServerProtocolTests
{
......@@ -36,6 +36,86 @@ void M2()
AssertJsonEquals(expectedEdits, results.DocumentChanges.First().Edits);
}
[WpfFact]
public async Task TestRename_WithLinkedFilesAsync()
{
var markup =
@"class A
{
void {|caret:|}{|renamed:M|}()
{
}
void M2()
{
{|renamed:M|}()
}
}";
var workspaceXml =
$@"<Workspace>
<Project Language=""C#"" CommonReferences=""true"" AssemblyName=""CSProj"" PreprocessorSymbols=""Proj1"">
<Document FilePath = ""C:\C.cs""><![CDATA[{markup}]]></Document>
</Project>
<Project Language = ""C#"" CommonReferences=""true"" PreprocessorSymbols=""Proj2"">
<Document IsLinkFile = ""true"" LinkAssemblyName=""CSProj"" LinkFilePath=""C:\C.cs""/>
</Project>
</Workspace>";
using var workspace = CreateXmlTestWorkspace(workspaceXml, out var locations);
var renameLocation = locations["caret"].First();
var renameValue = "RENAME";
var expectedEdits = locations["renamed"].Select(location => new LSP.TextEdit() { NewText = renameValue, Range = location.Range });
var results = await RunRenameAsync(workspace.CurrentSolution, renameLocation, renameValue);
AssertJsonEquals(expectedEdits, results.DocumentChanges.First().Edits);
}
[WpfFact]
public async Task TestRename_WithLinkedFilesAndPreprocessorAsync()
{
var markup =
@"class A
{
void {|caret:|}{|renamed:M|}()
{
}
void M2()
{
{|renamed:M|}()
}
void M3()
{
#if Proj1
{|renamed:M|}()
#endif
}
void M4()
{
#if Proj2
{|renamed:M|}()
#endif
}
}";
var workspaceXml =
$@"<Workspace>
<Project Language=""C#"" CommonReferences=""true"" AssemblyName=""CSProj"" PreprocessorSymbols=""Proj1"">
<Document FilePath = ""C:\C.cs""><![CDATA[{markup}]]></Document>
</Project>
<Project Language = ""C#"" CommonReferences=""true"" PreprocessorSymbols=""Proj2"">
<Document IsLinkFile = ""true"" LinkAssemblyName=""CSProj"" LinkFilePath=""C:\C.cs""/>
</Project>
</Workspace>";
using var workspace = CreateXmlTestWorkspace(workspaceXml, out var locations);
var renameLocation = locations["caret"].First();
var renameValue = "RENAME";
var expectedEdits = locations["renamed"].Select(location => new LSP.TextEdit() { NewText = renameValue, Range = location.Range });
var results = await RunRenameAsync(workspace.CurrentSolution, renameLocation, renameValue);
AssertJsonEquals(expectedEdits, results.DocumentChanges.First().Edits);
}
private static LSP.RenameParams CreateRenameParams(LSP.Location location, string newName)
=> new LSP.RenameParams()
{
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册