sourcegit/src/Models/TextInlineChange.cs
leo 96d4150d26 project: reorganize the structure of the project.
* remove dotnet-tool.json because the project does not rely on any dotnet tools.
* remove Directory.Build.props because the solution has only one project.
* move src/SourceGit to src. It's not needed to put all sources into a subfolder of src since there's only one project.
2024-04-02 20:00:33 +08:00

328 lines
11 KiB
C#

using System.Collections.Generic;
namespace SourceGit.Models
{
public class TextInlineChange
{
public int DeletedStart { get; set; }
public int DeletedCount { get; set; }
public int AddedStart { get; set; }
public int AddedCount { get; set; }
class Chunk
{
public int Hash;
public bool Modified;
public int Start;
public int Size;
public Chunk(int hash, int start, int size)
{
Hash = hash;
Modified = false;
Start = start;
Size = size;
}
}
enum Edit
{
None,
DeletedRight,
DeletedLeft,
AddedRight,
AddedLeft,
}
class EditResult
{
public Edit State;
public int DeleteStart;
public int DeleteEnd;
public int AddStart;
public int AddEnd;
}
public TextInlineChange(int dp, int dc, int ap, int ac)
{
DeletedStart = dp;
DeletedCount = dc;
AddedStart = ap;
AddedCount = ac;
}
public static List<TextInlineChange> Compare(string oldValue, string newValue)
{
var hashes = new Dictionary<string, int>();
var chunksOld = MakeChunks(hashes, oldValue);
var chunksNew = MakeChunks(hashes, newValue);
var sizeOld = chunksOld.Count;
var sizeNew = chunksNew.Count;
var max = sizeOld + sizeNew + 2;
var forward = new int[max];
var reverse = new int[max];
CheckModified(chunksOld, 0, sizeOld, chunksNew, 0, sizeNew, forward, reverse);
var ret = new List<TextInlineChange>();
var posOld = 0;
var posNew = 0;
var last = null as TextInlineChange;
do
{
while (posOld < sizeOld && posNew < sizeNew && !chunksOld[posOld].Modified && !chunksNew[posNew].Modified)
{
posOld++;
posNew++;
}
var beginOld = posOld;
var beginNew = posNew;
var countOld = 0;
var countNew = 0;
for (; posOld < sizeOld && chunksOld[posOld].Modified; posOld++)
countOld += chunksOld[posOld].Size;
for (; posNew < sizeNew && chunksNew[posNew].Modified; posNew++)
countNew += chunksNew[posNew].Size;
if (countOld + countNew == 0)
continue;
var diff = new TextInlineChange(
countOld > 0 ? chunksOld[beginOld].Start : 0,
countOld,
countNew > 0 ? chunksNew[beginNew].Start : 0,
countNew);
if (last != null)
{
var midSizeOld = diff.DeletedStart - last.DeletedStart - last.DeletedCount;
var midSizeNew = diff.AddedStart - last.AddedStart - last.AddedCount;
if (midSizeOld == 1 && midSizeNew == 1)
{
last.DeletedCount += (1 + countOld);
last.AddedCount += (1 + countNew);
continue;
}
}
last = diff;
ret.Add(diff);
} while (posOld < sizeOld && posNew < sizeNew);
return ret;
}
private static List<Chunk> MakeChunks(Dictionary<string, int> hashes, string text)
{
var start = 0;
var size = text.Length;
var chunks = new List<Chunk>();
var delims = new HashSet<char>(" \t+-*/=!,:;.'\"/?|&#@%`<>()[]{}\\".ToCharArray());
for (int i = 0; i < size; i++)
{
var ch = text[i];
if (delims.Contains(ch))
{
if (start != i)
AddChunk(chunks, hashes, text.Substring(start, i - start), start);
AddChunk(chunks, hashes, text.Substring(i, 1), i);
start = i + 1;
}
}
if (start < size)
AddChunk(chunks, hashes, text.Substring(start), start);
return chunks;
}
private static void CheckModified(List<Chunk> chunksOld, int startOld, int endOld, List<Chunk> chunksNew, int startNew, int endNew, int[] forward, int[] reverse)
{
while (startOld < endOld && startNew < endNew && chunksOld[startOld].Hash == chunksNew[startNew].Hash)
{
startOld++;
startNew++;
}
while (startOld < endOld && startNew < endNew && chunksOld[endOld - 1].Hash == chunksNew[endNew - 1].Hash)
{
endOld--;
endNew--;
}
var lenOld = endOld - startOld;
var lenNew = endNew - startNew;
if (lenOld > 0 && lenNew > 0)
{
var rs = CheckModifiedEdit(chunksOld, startOld, endOld, chunksNew, startNew, endNew, forward, reverse);
if (rs.State == Edit.None)
return;
if (rs.State == Edit.DeletedRight && rs.DeleteStart - 1 > startOld)
{
chunksOld[--rs.DeleteStart].Modified = true;
}
else if (rs.State == Edit.DeletedLeft && rs.DeleteEnd < endOld)
{
chunksOld[rs.DeleteEnd++].Modified = true;
}
else if (rs.State == Edit.AddedRight && rs.AddStart - 1 > startNew)
{
chunksNew[--rs.AddStart].Modified = true;
}
else if (rs.State == Edit.AddedLeft && rs.AddEnd < endNew)
{
chunksNew[rs.AddEnd++].Modified = true;
}
CheckModified(chunksOld, startOld, rs.DeleteStart, chunksNew, startNew, rs.AddStart, forward, reverse);
CheckModified(chunksOld, rs.DeleteEnd, endOld, chunksNew, rs.AddEnd, endNew, forward, reverse);
}
else if (lenOld > 0)
{
for (int i = startOld; i < endOld; i++)
chunksOld[i].Modified = true;
}
else if (lenNew > 0)
{
for (int i = startNew; i < endNew; i++)
chunksNew[i].Modified = true;
}
}
private static EditResult CheckModifiedEdit(List<Chunk> chunksOld, int startOld, int endOld, List<Chunk> chunksNew, int startNew, int endNew, int[] forward, int[] reverse)
{
var lenOld = endOld - startOld;
var lenNew = endNew - startNew;
var max = lenOld + lenNew + 1;
var half = max / 2;
var delta = lenOld - lenNew;
var deltaEven = delta % 2 == 0;
var rs = new EditResult() { State = Edit.None };
forward[1 + half] = 0;
reverse[1 + half] = lenOld + 1;
for (int i = 0; i <= half; i++)
{
for (int j = -i; j <= i; j += 2)
{
var idx = j + half;
int o, n;
if (j == -i || (j != i && forward[idx - 1] < forward[idx + 1]))
{
o = forward[idx + 1];
rs.State = Edit.AddedRight;
}
else
{
o = forward[idx - 1] + 1;
rs.State = Edit.DeletedRight;
}
n = o - j;
var startX = o;
var startY = n;
while (o < lenOld && n < lenNew && chunksOld[o + startOld].Hash == chunksNew[n + startNew].Hash)
{
o++;
n++;
}
forward[idx] = o;
if (!deltaEven && j - delta >= -i + 1 && j - delta <= i - 1)
{
var revIdx = (j - delta) + half;
var revOld = reverse[revIdx];
int revNew = revOld - j;
if (revOld <= o && revNew <= n)
{
if (i == 0)
{
rs.State = Edit.None;
}
else
{
rs.DeleteStart = startX + startOld;
rs.DeleteEnd = o + startOld;
rs.AddStart = startY + startNew;
rs.AddEnd = n + startNew;
}
return rs;
}
}
}
for (int j = -i; j <= i; j += 2)
{
var idx = j + half;
int o, n;
if (j == -i || (j != i && reverse[idx + 1] <= reverse[idx - 1]))
{
o = reverse[idx + 1] - 1;
rs.State = Edit.DeletedLeft;
}
else
{
o = reverse[idx - 1];
rs.State = Edit.AddedLeft;
}
n = o - (j + delta);
var endX = o;
var endY = n;
while (o > 0 && n > 0 && chunksOld[startOld + o - 1].Hash == chunksNew[startNew + n - 1].Hash)
{
o--;
n--;
}
reverse[idx] = o;
if (deltaEven && j + delta >= -i && j + delta <= i)
{
var forIdx = (j + delta) + half;
var forOld = forward[forIdx];
int forNew = forOld - (j + delta);
if (forOld >= o && forNew >= n)
{
if (i == 0)
{
rs.State = Edit.None;
}
else
{
rs.DeleteStart = o + startOld;
rs.DeleteEnd = endX + startOld;
rs.AddStart = n + startNew;
rs.AddEnd = endY + startNew;
}
return rs;
}
}
}
}
rs.State = Edit.None;
return rs;
}
private static void AddChunk(List<Chunk> chunks, Dictionary<string, int> hashes, string data, int start)
{
int hash;
if (hashes.TryGetValue(data, out hash))
{
chunks.Add(new Chunk(hash, start, data.Length));
}
else
{
hash = hashes.Count;
hashes.Add(data, hash);
chunks.Add(new Chunk(hash, start, data.Length));
}
}
}
}