-
Notifications
You must be signed in to change notification settings - Fork 267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: LSP rename support #4365
Merged
Merged
feat: LSP rename support #4365
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
39f96f7
wip: LSP rename support
alex-chew 2cb7100
Merge branch 'master' into feat/lsp-rename
alex-chew 8c55054
fix some tests
alex-chew 0df9a9a
fix remaining test
alex-chew e246ab0
Merge branch 'master' into feat/lsp-rename
alex-chew 3c32413
clarify requirements for rename support
alex-chew 6f4926b
optimize trivial rename
alex-chew b1ab9c2
Merge branch 'master' into feat/lsp-rename
alex-chew 9bfbc7c
extract DocumentEdits utils
alex-chew 903cfdf
enhance DocumentEdits utils
alex-chew 134977c
add single-file rename tests
alex-chew 146a806
Merge branch 'master' into feat/lsp-rename
alex-chew 03fca79
WIP project file tests
alex-chew e22387a
add multi-file tests
alex-chew 64a4911
add implicit project test
alex-chew 09792f7
Merge branch 'master' into feat/lsp-rename
alex-chew a296f12
Merge branch 'master' into feat/lsp-rename
alex-chew 18091d6
add test for renaming a non-symbol
alex-chew 2c0c6a5
add release note
alex-chew 53ed2b0
cleanup unnecessary setup calls in tests
alex-chew d1fbad6
Merge branch 'master' into feat/lsp-rename
alex-chew 79039ca
Merge branch 'master' into feat/lsp-rename
alex-chew File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
185 changes: 185 additions & 0 deletions
185
Source/DafnyLanguageServer.Test/Refactoring/RenameTest.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using JetBrains.Annotations; | ||
using Microsoft.Dafny.LanguageServer.IntegrationTest.Util; | ||
using OmniSharp.Extensions.LanguageServer.Protocol.Document; | ||
using OmniSharp.Extensions.LanguageServer.Protocol.Models; | ||
using Xunit; | ||
using Xunit.Abstractions; | ||
using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range; | ||
|
||
namespace Microsoft.Dafny.LanguageServer.IntegrationTest.Refactoring { | ||
public class RenameTest : ClientBasedLanguageServerTest { | ||
[Fact] | ||
public async Task ImplicitProjectFails() { | ||
var source = @" | ||
const i := 0 | ||
".TrimStart(); | ||
|
||
var documentItem = await CreateAndOpenTestDocument(source); | ||
await Assert.ThrowsAnyAsync<Exception>(() => RequestRename(documentItem, new Position(0, 6), "j")); | ||
} | ||
|
||
[Fact] | ||
public async Task InvalidNewNameIsNoOp() { | ||
var documentItem = await CreateAndOpenTestDocument(""); | ||
var workspaceEdit = await RequestRename(documentItem, new Position(0, 0), ""); | ||
Assert.Null(workspaceEdit); | ||
} | ||
|
||
[Fact] | ||
public async Task RenameNonSymbolFails() { | ||
var tempDir = await SetUpProjectFile(); | ||
var documentItem = await CreateAndOpenTestDocument("module Foo {}", Path.Combine(tempDir, "tmp.dfy")); | ||
var workspaceEdit = await RequestRename(documentItem, new Position(0, 6), "space"); | ||
Assert.Null(workspaceEdit); | ||
} | ||
|
||
[Fact] | ||
public async Task RenameDeclarationRenamesUsages() { | ||
var source = @" | ||
const [>><i<] := 0 | ||
method M() { | ||
print [>i<] + [>i<]; | ||
} | ||
".TrimStart(); | ||
|
||
var tempDir = await SetUpProjectFile(); | ||
await AssertRangesRenamed(source, tempDir, "foobar"); | ||
} | ||
|
||
[Fact] | ||
public async Task RenameUsageRenamesDeclaration() { | ||
var source = @" | ||
method [>foobar<]() | ||
method U() { [>><foobar<](); } | ||
".TrimStart(); | ||
|
||
var tempDir = await SetUpProjectFile(); | ||
await AssertRangesRenamed(source, tempDir, "M"); | ||
} | ||
|
||
[Fact] | ||
public async Task RenameUsageRenamesOtherUsages() { | ||
var source = @" | ||
module [>A<] {} | ||
module B { import [>A<] } | ||
module C { import [>><A<] } | ||
module D { import [>A<] } | ||
".TrimStart(); | ||
|
||
var tempDir = await SetUpProjectFile(); | ||
await AssertRangesRenamed(source, tempDir, "AAA"); | ||
} | ||
|
||
[Fact] | ||
public async Task RenameDeclarationAcrossFiles() { | ||
var sourceA = @" | ||
module A { | ||
class [>><C<] {} | ||
} | ||
".TrimStart(); | ||
var sourceB = @" | ||
module B { | ||
import A | ||
method M(c: A.[>C<]) {} | ||
} | ||
".TrimStart(); | ||
|
||
var tempDir = await SetUpProjectFile(); | ||
await AssertRangesRenamed(new[] { sourceA, sourceB }, tempDir, "CCC"); | ||
} | ||
|
||
[Fact] | ||
public async Task RenameUsageAcrossFiles() { | ||
var sourceA = @" | ||
abstract module [>A<] {} | ||
".TrimStart(); | ||
var sourceB = @" | ||
abstract module B { import [>><A<] } | ||
".TrimStart(); | ||
|
||
var tempDir = await SetUpProjectFile(); | ||
await AssertRangesRenamed(new[] { sourceA, sourceB }, tempDir, "AAA"); | ||
} | ||
|
||
/// <summary> | ||
/// Create an empty project file in a new temporary directory, and return the temporary directory's path. | ||
/// </summary> | ||
protected async Task<string> SetUpProjectFile() { | ||
var tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); | ||
Directory.CreateDirectory(tempDir); | ||
var projectFilePath = Path.Combine(tempDir, DafnyProject.FileName); | ||
await File.WriteAllTextAsync(projectFilePath, ""); | ||
return tempDir; | ||
} | ||
|
||
protected override Task SetUp(Action<DafnyOptions> modifyOptions) { | ||
return base.SetUp(o => { | ||
o.Set(ServerCommand.ProjectMode, true); | ||
modifyOptions?.Invoke(o); | ||
}); | ||
} | ||
|
||
/// <summary> | ||
/// Assert that after requesting a rename to <paramref name="newName"/> | ||
/// at the markup position in <paramref name="source"/> | ||
/// (there must be exactly one markup position), | ||
/// all markup ranges are renamed. | ||
/// </summary> | ||
private async Task AssertRangesRenamed(string source, string tempDir, string newName) { | ||
await AssertRangesRenamed(new[] { source }, tempDir, newName); | ||
} | ||
|
||
private record DocPosRange(TextDocumentItem Document, [CanBeNull] Position Position, ImmutableArray<Range> Ranges); | ||
|
||
/// <summary> | ||
/// Assert that after requesting a rename to <paramref name="newName"/> | ||
/// at the markup position in <paramref name="sources"/> | ||
/// (there must be exactly one markup position among all sources), | ||
/// all markup ranges are renamed. | ||
/// </summary> | ||
private async Task AssertRangesRenamed(IEnumerable<string> sources, string tempDir, string newName) { | ||
var items = sources.Select(async (source, sourceIndex) => { | ||
MarkupTestFile.GetPositionsAndRanges(source, out var cleanSource, | ||
out var positions, out var ranges); | ||
var documentItem = await CreateAndOpenTestDocument(cleanSource, Path.Combine(tempDir, $"tmp{sourceIndex}.dfy")); | ||
Assert.InRange(positions.Count, 0, 1); | ||
return new DocPosRange(documentItem, positions.FirstOrDefault((Position)null), ranges); | ||
}).Select(task => task.Result).ToImmutableList(); | ||
|
||
var itemWithPos = items.Single(item => item.Position != null); | ||
var workspaceEdit = await RequestRename(itemWithPos.Document, itemWithPos.Position, newName); | ||
Assert.NotNull(workspaceEdit.Changes); | ||
|
||
foreach (var (document, _, ranges) in items) { | ||
Assert.Contains(document.Uri, workspaceEdit.Changes); | ||
var editedText = DocumentEdits.ApplyEdits(workspaceEdit.Changes[document.Uri], document.Text); | ||
var expectedChanges = ranges.Select(range => new TextEdit { | ||
Range = range, | ||
NewText = newName, | ||
}); | ||
var expectedText = DocumentEdits.ApplyEdits(expectedChanges, document.Text); | ||
Assert.Equal(expectedText, editedText); | ||
} | ||
} | ||
|
||
private async Task<WorkspaceEdit> RequestRename( | ||
TextDocumentItem documentItem, Position position, string newName) { | ||
await AssertNoResolutionErrors(documentItem); | ||
return await client.RequestRename( | ||
new RenameParams { | ||
TextDocument = documentItem.Uri, | ||
Position = position, | ||
NewName = newName, | ||
}, CancellationToken); | ||
} | ||
|
||
public RenameTest(ITestOutputHelper output) : base(output) { | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using OmniSharp.Extensions.LanguageServer.Protocol.Models; | ||
|
||
namespace Microsoft.Dafny.LanguageServer.IntegrationTest.Util; | ||
|
||
public class DocumentEdits { | ||
public static string ApplyEdits(TextDocumentEdit textDocumentEdit, string text) { | ||
return ApplyEdits(textDocumentEdit.Edits, text); | ||
} | ||
|
||
public static string ApplyEdits(IEnumerable<TextEdit> edits, string text) { | ||
var inversedEdits = edits.ToList() | ||
.OrderByDescending(x => x.Range.Start.Line) | ||
.ThenByDescending(x => x.Range.Start.Character); | ||
var modifiedText = ToLines(text); | ||
foreach (var textEdit in inversedEdits) { | ||
modifiedText = ApplyEditLinewise(modifiedText, textEdit.Range, textEdit.NewText); | ||
} | ||
|
||
return string.Join("\n", modifiedText); | ||
} | ||
|
||
|
||
public static List<string> ToLines(string text) { | ||
return text.ReplaceLineEndings("\n").Split("\n").ToList(); | ||
} | ||
|
||
public static string FromLines(List<string> lines) { | ||
return string.Join("\n", lines).ReplaceLineEndings("\n"); | ||
} | ||
|
||
public static string ApplyEdit(List<string> lines, Range range, string newText) { | ||
return FromLines(ApplyEditLinewise(lines, range, newText)); | ||
} | ||
|
||
public static List<string> ApplyEditLinewise(List<string> lines, Range range, string newText) { | ||
var lineStart = lines[range.Start.Line]; | ||
var lineEnd = lines[range.End.Line]; | ||
lines[range.Start.Line] = lineStart[..range.Start.Character] + newText + lineEnd[range.End.Character..]; | ||
lines = lines.Take(range.Start.Line).Concat(lines.Skip(range.End.Line)).ToList(); | ||
return lines; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if you do a
PrepareRename
call here? Can that one fail and succeed?