Skip to content
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

Replace the algorithm used to calculate semantic diff #2126

Merged
merged 11 commits into from Jun 30, 2020
Expand Up @@ -3,8 +3,8 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.AspNetCore.Razor.LanguageServer.Semantic.Models;
using System.Linq;

namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic.Services
{
ryanbrandenburg marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -16,84 +16,109 @@ internal static class SyntaxTokenToSemanticTokensEditHelper
SemanticTokens newTokens,
ryanbrandenburg marked this conversation as resolved.
Show resolved Hide resolved
IReadOnlyList<uint> previousResults)
{
var oldData = previousResults;

if (oldData is null || oldData.Count == 0)
var diffs = UIntArrayDiffer.GetEdits(previousResults.ToArray(), newTokens.Data);
var edits = ComputeSemanticTokenEdits(diffs, newTokens.Data);
ryanbrandenburg marked this conversation as resolved.
Show resolved Hide resolved
var result = new SemanticTokensEditCollection
{
return newTokens;
}
ResultId = newTokens.ResultId,
Edits = edits,
};

var prevData = oldData;
var prevDataLength = oldData.Count;
var dataLength = newTokens.Data.Length;
var startIndex = 0;
while (startIndex < dataLength
&& startIndex < prevDataLength
&& prevData[startIndex] == newTokens.Data[startIndex])
{
startIndex++;
}
return result;
}

if (startIndex < dataLength && startIndex < prevDataLength)
private static IReadOnlyList<SemanticTokensEdit> ComputeSemanticTokenEdits(IReadOnlyList<DiffEdit> diffs, uint[] newArray)
{
var results = new List<SemanticTokensEdit>();
foreach (var diff in diffs)
ryanbrandenburg marked this conversation as resolved.
Show resolved Hide resolved
{
// Find end index
var endIndex = 0;
while (endIndex < dataLength
&& endIndex < prevDataLength
&& prevData[prevDataLength - 1 - endIndex] == newTokens.Data[dataLength - 1 - endIndex])
{
endIndex++;
}

var newData = ImmutableArray.Create(newTokens.Data, startIndex, dataLength - endIndex - startIndex);
var result = new SemanticTokensEditCollection
var current = results.Any() ? results.Last() : null;
switch (diff.Operation)
{
ResultId = newTokens.ResultId,
Edits = new[] {
new SemanticTokensEdit {
Start = startIndex,
DeleteCount = prevDataLength - endIndex - startIndex,
Data = newData
case DiffEdit.Type.Delete:
if (current != null
&& current.DeleteCount > 0
&& current.Start + current.DeleteCount == diff.Position)
{
current.DeleteCount += 1;
}
else
{
results.Add(new SemanticTokensEdit
{
Start = diff.Position,
Data = new uint[] { },
DeleteCount = 1,
});
}
break;
case DiffEdit.Type.Insert:
if (current != null
&& current.Data.Count() > 0
ryanbrandenburg marked this conversation as resolved.
Show resolved Hide resolved
&& current.Start == diff.Position)
{
current.Data = current.Data.Append(newArray[diff.NewTextPosition.Value]);
}
}
};
return result;
else
{
results.Add(new SemanticTokensEdit
{
Start = diff.Position,
Data = new uint[] { newArray[diff.NewTextPosition.Value] },
DeleteCount = 0,
});
}
break;
}
}

if (startIndex < dataLength)
return results;
}
}

internal class UIntArrayDiffer : TextDiffer
{
private UIntArrayDiffer(uint[] oldArray, uint[] newArray)
{
OldArray = oldArray;
NewArray = newArray;
}

private uint[] OldArray { get; }
private uint[] NewArray { get; }

protected override int OldTextLength => OldArray.Count();
protected override int NewTextLength => NewArray.Count();

protected override bool ContentEquals(int oldTextIndex, int newTextIndex)
{
return OldArray[oldTextIndex] == NewArray[newTextIndex];
}

public static IReadOnlyList<DiffEdit> GetEdits(uint[] oldArray, uint[] newArray)
{
if (oldArray is null)
{
return new SemanticTokensEditCollection
{
ResultId = newTokens.ResultId,
Edits = new[] {
new SemanticTokensEdit {
Start = startIndex,
DeleteCount = 0,
Data = ImmutableArray.Create(newTokens.Data, startIndex, newTokens.Data.Length - startIndex)
}
}
};
throw new ArgumentNullException();
}

if (startIndex < prevDataLength)
if (newArray is null)
{
return new SemanticTokensEditCollection
{
ResultId = newTokens.ResultId,
Edits = new[] {
new SemanticTokensEdit {
Start = startIndex,
DeleteCount = prevDataLength - startIndex
}
}
};
throw new ArgumentNullException();
}

return new SemanticTokensEditCollection
if (oldArray.SequenceEqual(newArray))
{
ResultId = newTokens.ResultId,
Edits = Array.Empty<SemanticTokensEdit>()
};
return Array.Empty<DiffEdit>();
}
else if (oldArray.Length == 0 || newArray.Length == 0)
{
throw new NotImplementedException();
ryanbrandenburg marked this conversation as resolved.
Show resolved Hide resolved
}

var differ = new UIntArrayDiffer(oldArray, newArray);
var diffs = differ.ComputeDiff();

return diffs;
}
}
}
Expand Up @@ -287,6 +287,45 @@ public void GetSemanticTokens_Razor_OnlyDifferences_Internal()
Assert.NotEqual(previousResultId, newResultId);
}

[Fact]
public void GetSemanticTokens_Razor_Modify()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}" +
$"<test1 bool-val=\"true\" />{Environment.NewLine}" +
$"<test1 bool-val=\"true\" />{Environment.NewLine}" +
$"<test1 bool-val=\"true\" />{Environment.NewLine}";
var expectedData = new List<uint> {
1, 1, 5, 1, 0, //line, character pos, length, tokenType, modifier
0, 6, 8, 1, 0,
1, 1, 5, 1, 0,
0, 6, 8, 1, 0,
1, 1, 5, 1, 0,
0, 6, 8, 1, 0,
};

var previousResultId = AssertSemanticTokens(txt, expectedData, isRazor: false, out var service);

var newTxt = $"@addTagHelper *, TestAssembly{Environment.NewLine}" +
$"<test1 bool-va=\"true\" />{Environment.NewLine}" +
$"<test1 bool-val=\"true\" />{Environment.NewLine}" +
$"<test1 bool-val=\"true\" />{Environment.NewLine}";
var newExpectedData = new SemanticTokensEditCollection
{
Edits = new List<SemanticTokensEdit>
{
new SemanticTokensEdit
{
Start = 5,
Data = new uint[]{
},
DeleteCount = 5,
},
}
};
var newResultId = AssertSemanticTokenEdits(newTxt, newExpectedData, isRazor: false, previousResultId: previousResultId, out _, service);
Assert.NotEqual(previousResultId, newResultId);
}

[Fact]
public void GetSemanticTokens_Razor_OnlyDifferences_NewLines()
{
Expand Down
Expand Up @@ -24,6 +24,14 @@
</li>
</ul>
</div>
<SurveyPrompt Title="Foo" />
<NavLink Match="NavLinkMatch.All" />
ryanbrandenburg marked this conversation as resolved.
Show resolved Hide resolved
<NavLink Match="NavLinkMatch.All" />
<NavLink Match="NavLinkMatch.All" />
<NavLink Match="NavLinkMatch.All" />
<NavLink Match="NavLinkMatch.All" />
<NavLink Match="NavLinkMatch.All" />
<NavLink Match="NavLinkMatch.All" />

@functions {
bool collapseNavMenu = true;
Expand Down
16 changes: 16 additions & 0 deletions src/Razor/test/testapps/ComponentApp/Components/SurveyPrompt.razor
@@ -0,0 +1,16 @@
<div class="alert alert-secondary mt-4" role="alert">
<span class="oi oi-pencil mr-2" aria-hidden="true"></span>
<strong>@Title</strong>

<span class="text-nowrap">
Please take our
<a target="_blank" class="font-weight-bold" href="https://go.microsoft.com/fwlink/?linkid=2112271">brief survey</a>
</span>
and tell us what you think.
</div>

@code {
// Demonstrates how a parent component can supply parameters
[Parameter]
public string Title { get; set; }
}