From 54f7dc1ccc8c6d3c048c58836a69a7dce6c8720c Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 10 Nov 2024 16:14:34 -0800 Subject: [PATCH 01/17] Support X scrolling --- plugin/src/App/Components/VirtualScroller.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/src/App/Components/VirtualScroller.lua b/plugin/src/App/Components/VirtualScroller.lua index 466da8a4f..d6e68f9a9 100644 --- a/plugin/src/App/Components/VirtualScroller.lua +++ b/plugin/src/App/Components/VirtualScroller.lua @@ -15,8 +15,8 @@ local VirtualScroller = Roact.Component:extend("VirtualScroller") function VirtualScroller:init() self.scrollFrameRef = Roact.createRef() self:setState({ - WindowSize = Vector2.new(), - CanvasPosition = Vector2.new(), + WindowSize = Vector2.zero, + CanvasPosition = Vector2.zero, }) self.totalCanvas, self.setTotalCanvas = Roact.createBinding(0) @@ -134,7 +134,7 @@ function VirtualScroller:render() BackgroundColor3 = props.backgroundColor3 or theme.BorderedContainer.BackgroundColor, BorderColor3 = props.borderColor3 or theme.BorderedContainer.BorderColor, CanvasSize = self.totalCanvas:map(function(s) - return UDim2.fromOffset(0, s) + return UDim2.fromOffset(props.canvasWidth or 0, s) end), ScrollBarThickness = 9, ScrollBarImageColor3 = theme.ScrollBarColor, @@ -146,7 +146,7 @@ function VirtualScroller:render() BottomImage = Assets.Images.ScrollBar.Bottom, ElasticBehavior = Enum.ElasticBehavior.Always, - ScrollingDirection = Enum.ScrollingDirection.Y, + ScrollingDirection = Enum.ScrollingDirection.XY, VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar, [Roact.Ref] = self.scrollFrameRef, }, { From 8b8b77e64d5d8bb5dbb3761fa792360d40b486f1 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 10 Nov 2024 17:36:24 -0800 Subject: [PATCH 02/17] Upgrade Highlighter --- plugin/Packages/Highlighter | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/Packages/Highlighter b/plugin/Packages/Highlighter index e0d061449..d7473a878 160000 --- a/plugin/Packages/Highlighter +++ b/plugin/Packages/Highlighter @@ -1 +1 @@ -Subproject commit e0d061449ea5c4452ef77008b5197ae4d3d77621 +Subproject commit d7473a87807f30cdb5ee7268a560b5c33f22da67 From 3d075bd3890bc35209d93fef769facdf8c3ec748 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 10 Nov 2024 17:36:59 -0800 Subject: [PATCH 03/17] Improve string diff visualizer --- plugin/src/App/Components/CodeLabel.lua | 66 ------ .../Components/StringDiffVisualizer/init.lua | 220 +++++++++++++----- 2 files changed, 156 insertions(+), 130 deletions(-) delete mode 100644 plugin/src/App/Components/CodeLabel.lua diff --git a/plugin/src/App/Components/CodeLabel.lua b/plugin/src/App/Components/CodeLabel.lua deleted file mode 100644 index 1951106b9..000000000 --- a/plugin/src/App/Components/CodeLabel.lua +++ /dev/null @@ -1,66 +0,0 @@ -local Rojo = script:FindFirstAncestor("Rojo") -local Plugin = Rojo.Plugin -local Packages = Rojo.Packages - -local Roact = require(Packages.Roact) -local Highlighter = require(Packages.Highlighter) -Highlighter.matchStudioSettings() - -local e = Roact.createElement - -local Theme = require(Plugin.App.Theme) - -local CodeLabel = Roact.PureComponent:extend("CodeLabel") - -function CodeLabel:init() - self.labelRef = Roact.createRef() - self.highlightsRef = Roact.createRef() -end - -function CodeLabel:didMount() - Highlighter.highlight({ - textObject = self.labelRef:getValue(), - }) - self:updateHighlights() -end - -function CodeLabel:didUpdate() - self:updateHighlights() -end - -function CodeLabel:updateHighlights() - local highlights = self.highlightsRef:getValue() - if not highlights then - return - end - - for _, lineLabel in highlights:GetChildren() do - local lineNum = tonumber(string.match(lineLabel.Name, "%d+") or "0") - lineLabel.BackgroundColor3 = self.props.lineBackground - lineLabel.BorderSizePixel = 0 - lineLabel.BackgroundTransparency = if self.props.markedLines[lineNum] then 0.25 else 1 - end -end - -function CodeLabel:render() - return Theme.with(function(theme) - return e("TextLabel", { - Size = self.props.size, - Position = self.props.position, - Text = self.props.text, - BackgroundTransparency = 1, - FontFace = theme.Font.Code, - TextSize = theme.TextSize.Code, - TextXAlignment = Enum.TextXAlignment.Left, - TextYAlignment = Enum.TextYAlignment.Top, - TextColor3 = Color3.fromRGB(255, 255, 255), - [Roact.Ref] = self.labelRef, - }, { - SyntaxHighlights = e("Folder", { - [Roact.Ref] = self.highlightsRef, - }), - }) - end) -end - -return CodeLabel diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index a826c6496..59e87599e 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -5,15 +5,15 @@ local Packages = Rojo.Packages local Roact = require(Packages.Roact) local Log = require(Packages.Log) local Highlighter = require(Packages.Highlighter) +Highlighter.matchStudioSettings() local StringDiff = require(script:FindFirstChild("StringDiff")) local Timer = require(Plugin.Timer) local Theme = require(Plugin.App.Theme) local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync) -local CodeLabel = require(Plugin.App.Components.CodeLabel) local BorderedContainer = require(Plugin.App.Components.BorderedContainer) -local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) +local VirtualScroller = require(Plugin.App.Components.VirtualScroller) local e = Roact.createElement @@ -21,7 +21,8 @@ local StringDiffVisualizer = Roact.Component:extend("StringDiffVisualizer") function StringDiffVisualizer:init() self.scriptBackground, self.setScriptBackground = Roact.createBinding(Color3.fromRGB(0, 0, 0)) - self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0)) + self.updateEvent = Instance.new("BindableEvent") + self.lineHeight, self.setLineHeight = Roact.createBinding(15) -- Ensure that the script background is up to date with the current theme self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function() @@ -34,13 +35,14 @@ function StringDiffVisualizer:init() self:updateScriptBackground() self:setState({ - add = {}, - remove = {}, + oldDiffs = {}, + newDiffs = {}, }) end function StringDiffVisualizer:willUnmount() self.themeChangedConnection:Disconnect() + self.updateEvent:Destroy() end function StringDiffVisualizer:updateScriptBackground() @@ -52,10 +54,10 @@ end function StringDiffVisualizer:didUpdate(previousProps) if previousProps.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then - local add, remove = self:calculateDiffLines() + local oldDiffs, newDiffs = self:calculateDiffs() self:setState({ - add = add, - remove = remove, + oldDiffs = oldDiffs, + newDiffs = newDiffs, }) end end @@ -66,18 +68,16 @@ function StringDiffVisualizer:calculateContentSize(theme) local oldStringBounds = getTextBoundsAsync(oldString, theme.Font.Code, theme.TextSize.Code, math.huge) local newStringBounds = getTextBoundsAsync(newString, theme.Font.Code, theme.TextSize.Code, math.huge) - self.setContentSize( - Vector2.new(math.max(oldStringBounds.X, newStringBounds.X), math.max(oldStringBounds.Y, newStringBounds.Y)) - ) + return Vector2.new(math.max(oldStringBounds.X, newStringBounds.X), math.max(oldStringBounds.Y, newStringBounds.Y)) end -function StringDiffVisualizer:calculateDiffLines() - Timer.start("StringDiffVisualizer:calculateDiffLines") +function StringDiffVisualizer:calculateDiffs() + Timer.start("StringDiffVisualizer:calculateDiffs") local oldString, newString = self.props.oldString, self.props.newString -- Diff the two texts local startClock = os.clock() - local diffs = StringDiff.findDiffs(oldString, newString) + local diffs = StringDiff.findDiffs((string.gsub(oldString, "\t", " ")), (string.gsub(newString, "\t", " "))) local stopClock = os.clock() Log.trace( @@ -88,59 +88,85 @@ function StringDiffVisualizer:calculateDiffLines() #diffs ) - -- Determine which lines to highlight - local add, remove = {}, {} + -- Find the diff locations + local oldDiffs, newDiffs = {}, {} - local oldLineNum, newLineNum = 1, 1 + local oldLineNum, oldIdx, newLineNum, newIdx = 1, 0, 1, 0 for _, diff in diffs do local actionType, text = diff.actionType, diff.value - local lines = select(2, string.gsub(text, "\n", "\n")) + local lines = string.split(text, "\n") if actionType == StringDiff.ActionTypes.Equal then - oldLineNum += lines - newLineNum += lines + for i, line in lines do + if i > 1 then + oldLineNum += 1 + oldIdx = 0 + newLineNum += 1 + newIdx = 0 + end + oldIdx += #line + newIdx += #line + end elseif actionType == StringDiff.ActionTypes.Insert then - if lines > 0 then - local textLines = string.split(text, "\n") - for i, textLine in textLines do - if string.match(textLine, "%S") then - add[newLineNum + i - 1] = true - end + for i, line in lines do + if i > 1 then + newLineNum += 1 + newIdx = 0 end - else - if string.match(text, "%S") then - add[newLineNum] = true + if not newDiffs[newLineNum] then + newDiffs[newLineNum] = { + { start = newIdx, stop = newIdx + #line }, + } + else + table.insert(newDiffs[newLineNum], { + start = newIdx, + stop = newIdx + #line, + }) end + newIdx += #line end - newLineNum += lines elseif actionType == StringDiff.ActionTypes.Delete then - if lines > 0 then - local textLines = string.split(text, "\n") - for i, textLine in textLines do - if string.match(textLine, "%S") then - remove[oldLineNum + i - 1] = true - end + for i, line in lines do + if i > 1 then + oldLineNum += 1 + oldIdx = 0 end - else - if string.match(text, "%S") then - remove[oldLineNum] = true + if not oldDiffs[oldLineNum] then + oldDiffs[oldLineNum] = { + { start = oldIdx, stop = oldIdx + #line }, + } + else + table.insert(oldDiffs[oldLineNum], { + start = oldIdx, + stop = oldIdx + #line, + }) end + oldIdx += #line end - oldLineNum += lines else Log.warn("Unknown diff action: {} {}", actionType, text) end end Timer.stop() - return add, remove + return oldDiffs, newDiffs end function StringDiffVisualizer:render() local oldString, newString = self.props.oldString, self.props.newString + local oldDiffs, newDiffs = self.state.oldDiffs, self.state.newDiffs return Theme.with(function(theme) - self:calculateContentSize(theme) + self.setLineHeight(theme.TextSize.Code) + + local contentSize = self:calculateContentSize(theme) + + local richTextLinesOldString = Highlighter.buildRichTextLines({ + src = oldString, + }) + local richTextLinesNewString = Highlighter.buildRichTextLines({ + src = newString, + }) return e(BorderedContainer, { size = self.props.size, @@ -167,35 +193,101 @@ function StringDiffVisualizer:render() BackgroundColor3 = theme.BorderedContainer.BorderColor, BackgroundTransparency = 0.5, }), - Old = e(ScrollingFrame, { + Old = e(VirtualScroller, { position = UDim2.new(0, 2, 0, 2), size = UDim2.new(0.5, -7, 1, -4), - scrollingDirection = Enum.ScrollingDirection.XY, transparency = self.props.transparency, - contentSize = self.contentSize, - }, { - Source = e(CodeLabel, { - size = UDim2.new(1, 0, 1, 0), - position = UDim2.new(0, 0, 0, 0), - text = oldString, - lineBackground = theme.Diff.Remove, - markedLines = self.state.remove, - }), + count = #richTextLinesOldString, + updateEvent = self.updateEvent.Event, + canvasWidth = contentSize.X, + render = function(i) + local lineDiffs = oldDiffs[i] + local diffFrames = table.create(if lineDiffs then #lineDiffs else 0) + + if lineDiffs then + local charWidth = math.round(theme.TextSize.Code * 0.5) + for diffIdx, diff in lineDiffs do + local start, stop = diff.start, diff.stop + diffFrames[diffIdx] = e("Frame", { + Size = UDim2.new(0, math.max(charWidth * (stop - start), charWidth / 2), 1, 0), + Position = UDim2.fromOffset(charWidth * start, 0), + BackgroundColor3 = theme.Diff.Remove, + BackgroundTransparency = 0.75, + BorderSizePixel = 0, + ZIndex = -1, + }) + end + end + + return Roact.createFragment({ + CodeLabel = e("TextLabel", { + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0, 0), + Text = richTextLinesOldString[i], + RichText = true, + BackgroundColor3 = theme.Diff.Remove, + BackgroundTransparency = if lineDiffs then 0.85 else 1, + BorderSizePixel = 0, + FontFace = theme.Font.Code, + TextSize = theme.TextSize.Code, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = Color3.fromRGB(255, 255, 255), + }), + DiffFrames = Roact.createFragment(diffFrames), + }) + end, + getHeightBinding = function() + return self.lineHeight + end, }), - New = e(ScrollingFrame, { + New = e(VirtualScroller, { position = UDim2.new(0.5, 5, 0, 2), size = UDim2.new(0.5, -7, 1, -4), - scrollingDirection = Enum.ScrollingDirection.XY, transparency = self.props.transparency, - contentSize = self.contentSize, - }, { - Source = e(CodeLabel, { - size = UDim2.new(1, 0, 1, 0), - position = UDim2.new(0, 0, 0, 0), - text = newString, - lineBackground = theme.Diff.Add, - markedLines = self.state.add, - }), + count = #richTextLinesNewString, + updateEvent = self.updateEvent.Event, + canvasWidth = contentSize.X, + render = function(i) + local lineDiffs = newDiffs[i] + local diffFrames = table.create(if lineDiffs then #lineDiffs else 0) + + if lineDiffs then + local charWidth = math.round(theme.TextSize.Code * 0.5) + for diffIdx, diff in lineDiffs do + local start, stop = diff.start, diff.stop + diffFrames[diffIdx] = e("Frame", { + Size = UDim2.new(0, math.max(charWidth * (stop - start), charWidth / 2), 1, 0), + Position = UDim2.fromOffset(charWidth * start, 0), + BackgroundColor3 = theme.Diff.Add, + BackgroundTransparency = 0.75, + BorderSizePixel = 0, + ZIndex = -1, + }) + end + end + + return Roact.createFragment({ + CodeLabel = e("TextLabel", { + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0, 0), + Text = richTextLinesNewString[i], + RichText = true, + BackgroundColor3 = theme.Diff.Add, + BackgroundTransparency = if lineDiffs then 0.85 else 1, + BorderSizePixel = 0, + FontFace = theme.Font.Code, + TextSize = theme.TextSize.Code, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = Color3.fromRGB(255, 255, 255), + }), + DiffFrames = Roact.createFragment(diffFrames), + }) + end, + getHeightBinding = function() + return self.lineHeight + end, }), }) end) From dcdb400060e9f2d86a998a0e1437fd867b6e3824 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 10 Nov 2024 17:37:57 -0800 Subject: [PATCH 04/17] Port semantic diff cleanup --- .../StringDiffVisualizer/StringDiff.lua | 351 ++++++++++++++++-- 1 file changed, 316 insertions(+), 35 deletions(-) diff --git a/plugin/src/App/Components/StringDiffVisualizer/StringDiff.lua b/plugin/src/App/Components/StringDiffVisualizer/StringDiff.lua index 6632c006a..f8986895d 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/StringDiff.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/StringDiff.lua @@ -1,3 +1,4 @@ +--!strict --[[ Based on DiffMatchPatch by Neil Fraser. https://github.com/google/diff-match-patch @@ -67,11 +68,180 @@ function StringDiff.findDiffs(text1: string, text2: string): Diffs end -- Cleanup the diff + diffs = StringDiff._cleanupSemantic(diffs) diffs = StringDiff._reorderAndMerge(diffs) return diffs end +function StringDiff._computeDiff(text1: string, text2: string): Diffs + -- Assumes that the prefix and suffix have already been trimmed off + -- and shortcut returns have been made so these texts must be different + + local text1Length, text2Length = #text1, #text2 + + if text1Length == 0 then + -- It's simply inserting all of text2 into text1 + return { { actionType = StringDiff.ActionTypes.Insert, value = text2 } } + end + + if text2Length == 0 then + -- It's simply deleting all of text1 + return { { actionType = StringDiff.ActionTypes.Delete, value = text1 } } + end + + local longText = if text1Length > text2Length then text1 else text2 + local shortText = if text1Length > text2Length then text2 else text1 + local shortTextLength = #shortText + + -- Shortcut if the shorter string exists entirely inside the longer one + local indexOf = if shortTextLength == 0 then nil else string.find(longText, shortText, 1, true) + if indexOf ~= nil then + local diffs = { + { actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, 1, indexOf - 1) }, + { actionType = StringDiff.ActionTypes.Equal, value = shortText }, + { actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, indexOf + shortTextLength) }, + } + -- Swap insertions for deletions if diff is reversed + if text1Length > text2Length then + diffs[1].actionType, diffs[3].actionType = StringDiff.ActionTypes.Delete, StringDiff.ActionTypes.Delete + end + return diffs + end + + if shortTextLength == 1 then + -- Single character string + -- After the previous shortcut, the character can't be an equality + return { + { actionType = StringDiff.ActionTypes.Delete, value = text1 }, + { actionType = StringDiff.ActionTypes.Insert, value = text2 }, + } + end + + return StringDiff._bisect(text1, text2) +end + +function StringDiff._cleanupSemantic(diffs: Diffs): Diffs + -- Reduce the number of edits by eliminating semantically trivial equalities. + local changes = false + local equalities = {} -- Stack of indices where equalities are found. + local equalitiesLength = 0 -- Keeping our own length var is faster. + local lastEquality: string? = nil + -- Always equal to diffs[equalities[equalitiesLength]].value + local pointer = 1 -- Index of current position. + -- Number of characters that changed prior to the equality. + local length_insertions1 = 0 + local length_deletions1 = 0 + -- Number of characters that changed after the equality. + local length_insertions2 = 0 + local length_deletions2 = 0 + + while diffs[pointer] do + if diffs[pointer].actionType == StringDiff.ActionTypes.Equal then -- Equality found. + equalitiesLength = equalitiesLength + 1 + equalities[equalitiesLength] = pointer + length_insertions1 = length_insertions2 + length_deletions1 = length_deletions2 + length_insertions2 = 0 + length_deletions2 = 0 + lastEquality = diffs[pointer].value + else -- An insertion or deletion. + if diffs[pointer].actionType == StringDiff.ActionTypes.Insert then + length_insertions2 = length_insertions2 + #diffs[pointer].value + else + length_deletions2 = length_deletions2 + #diffs[pointer].value + end + -- Eliminate an equality that is smaller or equal to the edits on both + -- sides of it. + if + lastEquality + and (#lastEquality <= math.max(length_insertions1, length_deletions1)) + and (#lastEquality <= math.max(length_insertions2, length_deletions2)) + then + -- Duplicate record. + table.insert( + diffs, + equalities[equalitiesLength], + { actionType = StringDiff.ActionTypes.Delete, value = lastEquality } + ) + -- Change second copy to insert. + diffs[equalities[equalitiesLength] + 1].actionType = StringDiff.ActionTypes.Insert + -- Throw away the equality we just deleted. + equalitiesLength = equalitiesLength - 1 + -- Throw away the previous equality (it needs to be reevaluated). + equalitiesLength = equalitiesLength - 1 + pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0 + length_insertions1, length_deletions1 = 0, 0 -- Reset the counters. + length_insertions2, length_deletions2 = 0, 0 + lastEquality = nil + changes = true + end + end + pointer = pointer + 1 + end + + -- Normalize the diff. + if changes then + StringDiff._reorderAndMerge(diffs) + end + StringDiff._cleanupSemanticLossless(diffs) + + -- Find any overlaps between deletions and insertions. + -- e.g: abcxxxxxxdef + -- -> abcxxxdef + -- e.g: xxxabcdefxxx + -- -> defxxxabc + -- Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 2 + while diffs[pointer] do + if + diffs[pointer - 1].actionType == StringDiff.ActionTypes.Delete + and diffs[pointer].actionType == StringDiff.ActionTypes.Insert + then + local deletion = diffs[pointer - 1].value + local insertion = diffs[pointer].value + local overlap_length1 = StringDiff._commonOverlap(deletion, insertion) + local overlap_length2 = StringDiff._commonOverlap(insertion, deletion) + if overlap_length1 >= overlap_length2 then + if overlap_length1 >= #deletion / 2 or overlap_length1 >= #insertion / 2 then + -- Overlap found. Insert an equality and trim the surrounding edits. + table.insert( + diffs, + pointer, + { actionType = StringDiff.ActionTypes.Equal, value = string.sub(insertion, 1, overlap_length1) } + ) + diffs[pointer - 1].value = string.sub(deletion, 1, #deletion - overlap_length1) + diffs[pointer + 1].value = string.sub(insertion, overlap_length1 + 1) + pointer = pointer + 1 + end + else + if overlap_length2 >= #deletion / 2 or overlap_length2 >= #insertion / 2 then + -- Reverse overlap found. + -- Insert an equality and swap and trim the surrounding edits. + table.insert( + diffs, + pointer, + { actionType = StringDiff.ActionTypes.Equal, value = string.sub(deletion, 1, overlap_length2) } + ) + diffs[pointer - 1] = { + actionType = StringDiff.ActionTypes.Insert, + value = string.sub(insertion, 1, #insertion - overlap_length2), + } + diffs[pointer + 1] = { + actionType = StringDiff.ActionTypes.Delete, + value = string.sub(deletion, overlap_length2 + 1), + } + pointer = pointer + 1 + end + end + pointer = pointer + 1 + end + pointer = pointer + 1 + end + + return diffs +end + function StringDiff._sharedPrefix(text1: string, text2: string): number -- Uses a binary search to find the largest common prefix between the two strings -- Performance analysis: http://neil.fraser.name/news/2007/10/09/ @@ -124,51 +294,162 @@ function StringDiff._sharedSuffix(text1: string, text2: string): number return pointerMid end -function StringDiff._computeDiff(text1: string, text2: string): Diffs - -- Assumes that the prefix and suffix have already been trimmed off - -- and shortcut returns have been made so these texts must be different - - local text1Length, text2Length = #text1, #text2 +function StringDiff._commonOverlap(text1: string, text2: string): number + -- Determine if the suffix of one string is the prefix of another. - if text1Length == 0 then - -- It's simply inserting all of text2 into text1 - return { { actionType = StringDiff.ActionTypes.Insert, value = text2 } } + -- Cache the text lengths to prevent multiple calls. + local text1_length = #text1 + local text2_length = #text2 + -- Eliminate the null case. + if text1_length == 0 or text2_length == 0 then + return 0 + end + -- Truncate the longer string. + if text1_length > text2_length then + text1 = string.sub(text1, text1_length - text2_length + 1) + elseif text1_length < text2_length then + text2 = string.sub(text2, 1, text1_length) + end + local text_length = math.min(text1_length, text2_length) + -- Quick check for the worst case. + if text1 == text2 then + return text_length end - if text2Length == 0 then - -- It's simply deleting all of text1 - return { { actionType = StringDiff.ActionTypes.Delete, value = text1 } } + -- Start by looking for a single character match + -- and increase length until no match is found. + -- Performance analysis: https://neil.fraser.name/news/2010/11/04/ + local best = 0 + local length = 1 + while true do + local pattern = string.sub(text1, text_length - length + 1) + local found = string.find(text2, pattern, 1, true) + if found == nil then + return best + end + length = length + found - 1 + if found == 1 or string.sub(text1, text_length - length + 1) == string.sub(text2, 1, length) then + best = length + length = length + 1 + end end +end - local longText = if text1Length > text2Length then text1 else text2 - local shortText = if text1Length > text2Length then text2 else text1 - local shortTextLength = #shortText +function StringDiff._cleanupSemanticScore(one: string, two: string): number + -- Given two strings, compute a score representing whether the internal + -- boundary falls on logical boundaries. + -- Scores range from 6 (best) to 0 (worst). - -- Shortcut if the shorter string exists entirely inside the longer one - local indexOf = if shortTextLength == 0 then nil else string.find(longText, shortText, 1, true) - if indexOf ~= nil then - local diffs = { - { actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, 1, indexOf - 1) }, - { actionType = StringDiff.ActionTypes.Equal, value = shortText }, - { actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, indexOf + shortTextLength) }, - } - -- Swap insertions for deletions if diff is reversed - if text1Length > text2Length then - diffs[1].actionType, diffs[3].actionType = StringDiff.ActionTypes.Delete, StringDiff.ActionTypes.Delete - end - return diffs + if (#one == 0) or (#two == 0) then + -- Edges are the best. + return 6 end - if shortTextLength == 1 then - -- Single character string - -- After the previous shortcut, the character can't be an equality - return { - { actionType = StringDiff.ActionTypes.Delete, value = text1 }, - { actionType = StringDiff.ActionTypes.Insert, value = text2 }, - } + -- Each port of this function behaves slightly differently due to + -- subtle differences in each language's definition of things like + -- 'whitespace'. Since this function's purpose is largely cosmetic, + -- the choice has been made to use each language's native features + -- rather than force total conformity. + local char1 = string.sub(one, -1) + local char2 = string.sub(two, 1, 1) + local nonAlphaNumeric1 = string.match(char1, "%W") + local nonAlphaNumeric2 = string.match(char2, "%W") + local whitespace1 = nonAlphaNumeric1 and string.match(char1, "%s") + local whitespace2 = nonAlphaNumeric2 and string.match(char2, "%s") + local lineBreak1 = whitespace1 and string.match(char1, "%c") + local lineBreak2 = whitespace2 and string.match(char2, "%c") + local blankLine1 = lineBreak1 and string.match(one, "\n\r?\n$") + local blankLine2 = lineBreak2 and string.match(two, "^\r?\n\r?\n") + + if blankLine1 or blankLine2 then + -- Five points for blank lines. + return 5 + elseif lineBreak1 or lineBreak2 then + -- Four points for line breaks + -- DEVIATION: Prefer to start on a line break instead of end on it + return if lineBreak1 then 4 else 4.5 + elseif nonAlphaNumeric1 and not whitespace1 and whitespace2 then + -- Three points for end of sentences. + return 3 + elseif whitespace1 or whitespace2 then + -- Two points for whitespace. + return 2 + elseif nonAlphaNumeric1 or nonAlphaNumeric2 then + -- One point for non-alphanumeric. + return 1 end + return 0 +end - return StringDiff._bisect(text1, text2) +function StringDiff._cleanupSemanticLossless(diffs: Diffs) + -- Look for single edits surrounded on both sides by equalities + -- which can be shifted sideways to align the edit to a word boundary. + -- e.g: The cat came. -> The cat came. + + local pointer = 2 + -- Intentionally ignore the first and last element (don't need checking). + while diffs[pointer + 1] do + local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1] + if + (prevDiff.actionType == StringDiff.ActionTypes.Equal) + and (nextDiff.actionType == StringDiff.ActionTypes.Equal) + then + -- This is a single edit surrounded by equalities. + local diff = diffs[pointer] + + local equality1 = prevDiff.value + local edit = diff.value + local equality2 = nextDiff.value + + -- First, shift the edit as far left as possible. + local commonOffset = StringDiff._sharedSuffix(equality1, edit) + if commonOffset > 0 then + local commonString = string.sub(edit, -commonOffset) + equality1 = string.sub(equality1, 1, -commonOffset - 1) + edit = commonString .. string.sub(edit, 1, -commonOffset - 1) + equality2 = commonString .. equality2 + end + + -- Second, step character by character right, looking for the best fit. + local bestEquality1 = equality1 + local bestEdit = edit + local bestEquality2 = equality2 + local bestScore = StringDiff._cleanupSemanticScore(equality1, edit) + + StringDiff._cleanupSemanticScore(edit, equality2) + + while string.byte(edit, 1) == string.byte(equality2, 1) do + equality1 = equality1 .. string.sub(edit, 1, 1) + edit = string.sub(edit, 2) .. string.sub(equality2, 1, 1) + equality2 = string.sub(equality2, 2) + local score = StringDiff._cleanupSemanticScore(equality1, edit) + + StringDiff._cleanupSemanticScore(edit, equality2) + -- The >= encourages trailing rather than leading whitespace on edits. + if score >= bestScore then + bestScore = score + bestEquality1 = equality1 + bestEdit = edit + bestEquality2 = equality2 + end + end + if prevDiff.value ~= bestEquality1 then + -- We have an improvement, save it back to the diff. + if #bestEquality1 > 0 then + diffs[pointer - 1].value = bestEquality1 + else + table.remove(diffs, pointer - 1) + pointer = pointer - 1 + end + diffs[pointer].value = bestEdit + if #bestEquality2 > 0 then + diffs[pointer + 1].value = bestEquality2 + else + table.remove(diffs, pointer + 1) + pointer = pointer - 1 + end + end + end + pointer = pointer + 1 + end end function StringDiff._bisect(text1: string, text2: string): Diffs From af04038e626f33a51a66da7666af6dfac095591a Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 10 Nov 2024 19:00:38 -0800 Subject: [PATCH 05/17] Support setting canvas pos --- plugin/src/App/Components/VirtualScroller.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugin/src/App/Components/VirtualScroller.lua b/plugin/src/App/Components/VirtualScroller.lua index d6e68f9a9..a60cd69c8 100644 --- a/plugin/src/App/Components/VirtualScroller.lua +++ b/plugin/src/App/Components/VirtualScroller.lua @@ -41,6 +41,10 @@ function VirtualScroller:didMount() local canvasPositionSignal = rbx:GetPropertyChangedSignal("CanvasPosition") self.canvasPositionChanged = canvasPositionSignal:Connect(function() + if self.props.onCanvasPositionChanged then + pcall(self.props.onCanvasPositionChanged, rbx.CanvasPosition) + end + if math.abs(rbx.CanvasPosition.Y - self.state.CanvasPosition.Y) > 5 then self:setState({ CanvasPosition = rbx.CanvasPosition }) self:refresh() @@ -136,6 +140,7 @@ function VirtualScroller:render() CanvasSize = self.totalCanvas:map(function(s) return UDim2.fromOffset(props.canvasWidth or 0, s) end), + CanvasPosition = self.props.canvasPosition, ScrollBarThickness = 9, ScrollBarImageColor3 = theme.ScrollBarColor, ScrollBarImageTransparency = props.transparency:map(function(value) From aea67f2521e2f53b8a24026aef1a1e22ab760b68 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 10 Nov 2024 19:01:15 -0800 Subject: [PATCH 06/17] Way too much in one commit, sorry --- .../Components/StringDiffVisualizer/init.lua | 141 ++++++++++++++++-- 1 file changed, 125 insertions(+), 16 deletions(-) diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index 59e87599e..4d44cdf08 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -23,12 +23,15 @@ function StringDiffVisualizer:init() self.scriptBackground, self.setScriptBackground = Roact.createBinding(Color3.fromRGB(0, 0, 0)) self.updateEvent = Instance.new("BindableEvent") self.lineHeight, self.setLineHeight = Roact.createBinding(15) + self.canvasPosition, self.setCanvasPosition = Roact.createBinding(Vector2.zero) -- Ensure that the script background is up to date with the current theme self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function() - task.defer(function() - -- Defer to allow Highlighter to process the theme change first + task.delay(1 / 20, function() + -- Delay to allow Highlighter to process the theme change first self:updateScriptBackground() + -- Refresh the code label colors too + self.updateEvent:Fire() end) end) @@ -37,6 +40,8 @@ function StringDiffVisualizer:init() self:setState({ oldDiffs = {}, newDiffs = {}, + oldSpacers = {}, + newSpacers = {}, }) end @@ -54,11 +59,7 @@ end function StringDiffVisualizer:didUpdate(previousProps) if previousProps.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then - local oldDiffs, newDiffs = self:calculateDiffs() - self:setState({ - oldDiffs = oldDiffs, - newDiffs = newDiffs, - }) + self:updateDiffs() end end @@ -71,8 +72,8 @@ function StringDiffVisualizer:calculateContentSize(theme) return Vector2.new(math.max(oldStringBounds.X, newStringBounds.X), math.max(oldStringBounds.Y, newStringBounds.Y)) end -function StringDiffVisualizer:calculateDiffs() - Timer.start("StringDiffVisualizer:calculateDiffs") +function StringDiffVisualizer:updateDiffs() + Timer.start("StringDiffVisualizer:updateDiffs") local oldString, newString = self.props.oldString, self.props.newString -- Diff the two texts @@ -90,6 +91,9 @@ function StringDiffVisualizer:calculateDiffs() -- Find the diff locations local oldDiffs, newDiffs = {}, {} + local oldSpacers, newSpacers = {}, {} + + local firstDiffLineNum = 0 local oldLineNum, oldIdx, newLineNum, newIdx = 1, 0, 1, 0 for _, diff in diffs do @@ -108,10 +112,16 @@ function StringDiffVisualizer:calculateDiffs() newIdx += #line end elseif actionType == StringDiff.ActionTypes.Insert then + if firstDiffLineNum == 0 then + firstDiffLineNum = newLineNum + end + for i, line in lines do if i > 1 then newLineNum += 1 newIdx = 0 + + table.insert(oldSpacers, { oldLineNum = oldLineNum, newLineNum = newLineNum }) end if not newDiffs[newLineNum] then newDiffs[newLineNum] = { @@ -126,10 +136,16 @@ function StringDiffVisualizer:calculateDiffs() newIdx += #line end elseif actionType == StringDiff.ActionTypes.Delete then + if firstDiffLineNum == 0 then + firstDiffLineNum = oldLineNum + end + for i, line in lines do if i > 1 then oldLineNum += 1 oldIdx = 0 + + table.insert(newSpacers, { oldLineNum = oldLineNum, newLineNum = newLineNum }) end if not oldDiffs[oldLineNum] then oldDiffs[oldLineNum] = { @@ -149,18 +165,25 @@ function StringDiffVisualizer:calculateDiffs() end Timer.stop() - return oldDiffs, newDiffs + + self:setState({ + oldDiffs = oldDiffs, + newDiffs = newDiffs, + oldSpacers = oldSpacers, + newSpacers = newSpacers, + }) + -- Scroll to the first diff line + self.setCanvasPosition(Vector2.new(0, math.max(0, (firstDiffLineNum - 4) * 16))) end function StringDiffVisualizer:render() local oldString, newString = self.props.oldString, self.props.newString local oldDiffs, newDiffs = self.state.oldDiffs, self.state.newDiffs + local oldSpacers, newSpacers = self.state.oldSpacers, self.state.newSpacers return Theme.with(function(theme) self.setLineHeight(theme.TextSize.Code) - local contentSize = self:calculateContentSize(theme) - local richTextLinesOldString = Highlighter.buildRichTextLines({ src = oldString, }) @@ -168,6 +191,60 @@ function StringDiffVisualizer:render() src = newString, }) + local maxLines = math.max(#richTextLinesOldString, #richTextLinesNewString) + + -- Calculate the width of the canvas + -- (One line at a time to avoid the 200k char limit of getTextBoundsAsync) + local canvasWidth = 0 + for i = 1, maxLines do + local oldLine = richTextLinesOldString[i] + if oldLine and oldLine ~= "" then + local bounds = getTextBoundsAsync(oldLine, theme.Font.Code, theme.TextSize.Code, math.huge, true) + if bounds.X > canvasWidth then + canvasWidth = bounds.X + end + end + local newLine = richTextLinesNewString[i] + if newLine and oldLine ~= "" then + local bounds = getTextBoundsAsync(newLine, theme.Font.Code, theme.TextSize.Code, math.huge, true) + if bounds.X > canvasWidth then + canvasWidth = bounds.X + end + end + end + + -- Adjust the rich text lines and their diffs to include spacers (aka nil lines) + for spacerIdx, spacer in oldSpacers do + local spacerLineNum = spacer.oldLineNum + (spacerIdx - 1) + table.insert(richTextLinesOldString, spacerLineNum, nil) + -- The oldDiffs that come after this spacer need to be moved down + -- without overwriting the oldDiffs that are already there + local updatedOldDiffs = {} + for lineNum, diffs in pairs(oldDiffs) do + if lineNum >= spacerLineNum then + updatedOldDiffs[lineNum + 1] = diffs + else + updatedOldDiffs[lineNum] = diffs + end + end + oldDiffs = updatedOldDiffs + end + for spacerIdx, spacer in newSpacers do + local spacerLineNum = spacer.newLineNum + (spacerIdx - 1) + table.insert(richTextLinesNewString, spacerLineNum, nil) + -- The newDiffs that come after this spacer need to be moved down + -- without overwriting the newDiffs that are already there + local updatedNewDiffs = {} + for lineNum, diffs in pairs(newDiffs) do + if lineNum >= spacerLineNum then + updatedNewDiffs[lineNum + 1] = diffs + else + updatedNewDiffs[lineNum] = diffs + end + end + newDiffs = updatedNewDiffs + end + return e(BorderedContainer, { size = self.props.size, position = self.props.position, @@ -197,10 +274,26 @@ function StringDiffVisualizer:render() position = UDim2.new(0, 2, 0, 2), size = UDim2.new(0.5, -7, 1, -4), transparency = self.props.transparency, - count = #richTextLinesOldString, + count = maxLines, updateEvent = self.updateEvent.Event, - canvasWidth = contentSize.X, + canvasWidth = canvasWidth, + canvasPosition = self.canvasPosition, + onCanvasPositionChanged = self.setCanvasPosition, render = function(i) + if not richTextLinesOldString[i] then + return e("ImageLabel", { + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = "rbxassetid://117018699617466", + ImageTransparency = 0.7, + ImageColor3 = theme.TextColor, + ScaleType = Enum.ScaleType.Tile, + TileSize = UDim2.new(0, 64, 4, 0), + }) + end + local lineDiffs = oldDiffs[i] local diffFrames = table.create(if lineDiffs then #lineDiffs else 0) @@ -245,10 +338,26 @@ function StringDiffVisualizer:render() position = UDim2.new(0.5, 5, 0, 2), size = UDim2.new(0.5, -7, 1, -4), transparency = self.props.transparency, - count = #richTextLinesNewString, + count = maxLines, updateEvent = self.updateEvent.Event, - canvasWidth = contentSize.X, + canvasWidth = canvasWidth, + canvasPosition = self.canvasPosition, + onCanvasPositionChanged = self.setCanvasPosition, render = function(i) + if not richTextLinesNewString[i] then + return e("ImageLabel", { + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0, 0), + BackgroundTransparency = 1, + BorderSizePixel = 0, + Image = "rbxassetid://117018699617466", + ImageTransparency = 0.7, + ImageColor3 = theme.TextColor, + ScaleType = Enum.ScaleType.Tile, + TileSize = UDim2.new(0, 64, 4, 0), + }) + end + local lineDiffs = newDiffs[i] local diffFrames = table.create(if lineDiffs then #lineDiffs else 0) From 53c9521e402f317994d4db6f4c6769635e48ec62 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 10 Nov 2024 19:20:35 -0800 Subject: [PATCH 07/17] Make newline diffs less annoying --- .../Components/StringDiffVisualizer/init.lua | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index 4d44cdf08..90f6f49bd 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -164,6 +164,36 @@ function StringDiffVisualizer:updateDiffs() end end + -- Filter out diffs that are just newlines being added/removed from existing non-empty lines. + -- This is done to make the diff visualization less noisy. + + local oldStringLines = string.split(oldString, "\n") + local newStringLines = string.split(newString, "\n") + + for lineNum, lineDiffs in oldDiffs do + if + (#lineDiffs > 1) -- Not just newline + or (lineDiffs[1].start ~= lineDiffs[1].stop) -- Not a newline at all + or (oldStringLines[lineNum] == "") -- Empty line, so the newline change is significant + then + continue + end + -- Just a noisy newline diff, clear it + oldDiffs[lineNum] = nil + end + + for lineNum, lineDiffs in newDiffs do + if + (#lineDiffs > 1) -- Not just newline + or (lineDiffs[1].start ~= lineDiffs[1].stop) -- Not a newline at all + or (newStringLines[lineNum] == "") -- Empty line, so the newline change is significant + then + continue + end + -- Just a noisy newline diff, clear it + newDiffs[lineNum] = nil + end + Timer.stop() self:setState({ @@ -302,7 +332,7 @@ function StringDiffVisualizer:render() for diffIdx, diff in lineDiffs do local start, stop = diff.start, diff.stop diffFrames[diffIdx] = e("Frame", { - Size = UDim2.new(0, math.max(charWidth * (stop - start), charWidth / 2), 1, 0), + Size = UDim2.new(0, math.max(charWidth * (stop - start), charWidth * 0.4), 1, 0), Position = UDim2.fromOffset(charWidth * start, 0), BackgroundColor3 = theme.Diff.Remove, BackgroundTransparency = 0.75, @@ -366,7 +396,7 @@ function StringDiffVisualizer:render() for diffIdx, diff in lineDiffs do local start, stop = diff.start, diff.stop diffFrames[diffIdx] = e("Frame", { - Size = UDim2.new(0, math.max(charWidth * (stop - start), charWidth / 2), 1, 0), + Size = UDim2.new(0, math.max(charWidth * (stop - start), charWidth * 0.4), 1, 0), Position = UDim2.fromOffset(charWidth * start, 0), BackgroundColor3 = theme.Diff.Add, BackgroundTransparency = 0.75, From fede4ff86c6a0b36b6f0105bc2ada9a346612d22 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Sun, 10 Nov 2024 20:10:50 -0800 Subject: [PATCH 08/17] Add scrollmarkers and the image asset --- assets/images/diagonal-lines.png | Bin 0 -> 4538 bytes .../Components/StringDiffVisualizer/init.lua | 306 ++++++++++-------- plugin/src/Assets.lua | 1 + 3 files changed, 179 insertions(+), 128 deletions(-) create mode 100644 assets/images/diagonal-lines.png diff --git a/assets/images/diagonal-lines.png b/assets/images/diagonal-lines.png new file mode 100644 index 0000000000000000000000000000000000000000..5e12b36de51639bd7dd3d836f71ea8a10d43aca6 GIT binary patch literal 4538 zcmYjVe>_wB|EFQ0+U=)OS-+4y zQ`c_08M`H6jO0he&gWK}HrjGb=P*0pqsQa>eSH5oKi=n$_xtsJy`InK>-j#{4*7X5 zG%+{P)6-k%b-*J4dtRQq<{M%67o^ke*h4=)z;ln@>wUAoC!r$3>;s-G z0BK%**x-!WrA6;gI9@cEIjue$-S}Ua=ot6fQcb$Fa!pmyu3tX2-QM_=d}G`nG&yHn zk7neH_?s;r7p=PUNZDwpy)HoyrG?a+Gb5T&jw1R>`vq|5=%jbNfiyK4kT z95#;Dw*rVHoV5Gr&u=R3%yO;ZWfx@J@_;1}Wf_zauiogw8`5nB6Hd#v!mZ+b#-Jn< zS+eZJI9I=K?ijFn1J;zaCD)jV>T1QRU5my(+f@5VHyiSB<3g&-8l0G~DS&qF~lLVJpyaasvDjyUg}jI~XMi;{QuSB-od6M6D$ z47ss0Irr$JskE6j_s>QT@r-n;D(59IX|8p?M-NY*h~d_E>dLpi0-|d` z*4BW&om(J^3FHWSYhFHg>>c`{tsGAby-j)34kWNv0BZu7JXN2{PONz%jdBQ zc)O0oZyX3`a&Ig2KX8@F)1FSQvi`f2zKthoYRZpLh6CKxpJI=?X)K-=W{wzeUAo1A z#TaPD>O@4#-VbYR(QuL=OkL-@1ZYbW1-qdSf9Er!Wka0G_sK&`q1zWME9vW;;PMK3 z659Q3WXd@=T=*}tc)avsQRvbI3n7XfMA-sSD^Bf%GV)Uf!p(r=;yEOj&4=om4bZ)V z?l`}r9aDyTCz*tg_vsZ}c~m9+Db=Hzzupnz8uGtdD+c{GEEuAfqB}L-mGpNKNE9B> z;C(?x?r8sY9}!_QpX#e3BK$K zuPI*vl=Y_gopWIi_ovvxlWwTc{(rMn$mJ-yg)gv;<}eoVXDgR!S3s64`L`4ee(tug zcUpt%AOv9Kl(qo(5~?eQgawQx2}uOyra{5lX5eoqcP2Jf10__@*UIilji9Uw`hoK@ zG$+ZQHN_-Ql5xAMmY$cU|ADN#jd*LkNfM%MFOH*QZ4kGtSPIyq#~*)vn53>7){DhU z9)j7SvWsf@ABr3QXbtaU2y*J(TWbSL^&0VL-amQUWaa&!GDU~mO$}^Vc<1>Iu)nI5 zjrgi5(C)wTt|DP-soSI@r11`Hl_Sjo?u#s0v;g`WAo1n0~$nP-oE9U zbf7JyIvk^~E0VW;TBx(L>cwsEc0BnAPd}%2gz=7?-~(SfV+?RM7PePH-Wmrg;qf5<0qJIhyQv-sT#QoqhojG8xgyiyvYzj zc>1j%!t9zAK;wPYxEz^ZFB0R2!9dYm)n7gj2;n;gHf)o94}Qi9y&LHMl8h)p997;Q zM7u+s2>LflXuuNklanmcIEotD=e*Ox0Qd0`Uti61UudR=ou1k01dD>$E+P?ry}Zs; zxJ}m62Uor~Ce?p0sMbE90sPpKp6?gw$_t*kSRFh?6MAW0%_qefQa_;CegjLcn`_CcG|CG-T0y%|R=sO~SB$crXDHn)rk<qhhQv<=pns9# zj(-H{N{oHuvFgm+NZ~izlhG_D;Z=n2*Q>|HFD#a7$K=S!1#uSnzhf6 zc+Y_GG)VHrO|TB?mSi9~74(5Ou`~1KZ#n3Gjt9ysiC0s$L!MrmkKA9{$%2Y|^d}Ld zvxZdE^DQALuJxFhDt*K0VG_DM?lw%LOVt_MU+2{okX!p=+Tx>h=c-Dl=K|gig_x$> zIGtS}W9KQ}eXr0CFSSjJL8G5Wrm-a4jqeurpdH3t;iQ%3+K!r`<09)Wj2kSGr>sf) zNRBkQB6XGy+@$;H8ds@W@iKb_G*+1Deigx|DU&)w1Qc0drwK`&E-5#Oct2(Bqv`rC zsAsSUEDo%vs_WUEt=bbmHaT}H4Y>u>O}#IU?u5hVDozPd+u#eSStSKhyq=B74C?16 z$cq(MN)_J8b&FSqZP8IWgBX@8M-*|^rN5?BR<5*?+zaX~g+)pjSP`mt|EWFK4u5+KdqY=*x<#FEw|$+3|pvX@P=c74Qmf-Hh)8; zJ^Rv&vuLvN+FKx36w%C>ce z*SYp$y_~$Lv}ww09d%raZyHD#DMh|JE-MU51)%CnUyQ-4aR7Z_yw!m=*OBNT{b^9zUIjZ_4y6mBN(E0LYP1#4w*qA zqWlLepY&4N3Lx}Dk>VMPXIx>3E&56NUSF)j4g5daGki7w=rpdEu_m@wwy{WScF~j5 z$$IBN92*G{%%2X$j4V4WhrmVoRgg#TSWz4k$5ov6UqmX{Tb;b3ICjQQhSQ*n6~u=a z+1fBrWqL8$*jXWmHf+?2L9n7wbx^a*LnqZI-#wKo;uR8p-$4x71`#}}TkI_{;a~pe7E9Jnf@oPE{i-Mg5nLblfPz%=Wl1>O4m$4#*0Xr3Sit2$HLyy>ZVS zk;7v5-lg9OB%HGj1u6(#lxR_|`&#|kQ50vsn^SI=hNYuFNTQ-XOv79^ZsKfKf?#~L zY-qM6=@<)~NxS?8qZmOPB52Ah0Jrx3tq16CD?ZcGXZo9dnDnfZmYde$ssFi8cNcjQ zJAf`@((BX0FII=EEOr}(>NM>;R3&t$<-wAlFa&x^#|PmJ22g~HF| zmIq*M3G-kxupi2gbFt%9C5%kWt~F2Y>kpI`_-c80|>@sQDi&_k}TmfOoqaB(|NH42>sq_pwyo;XK?;% zl;5en0cIhtU){L$K4v^x45;#GCZa$lj;O(OsI35%!yuxK@fD+YWw4HoO-4@Qz+ zpB%p^GNYGD@6pTcsm$=zBCOpl#53B0BueT0wk{R!dL0p41HXT1tud5(x^qvAK8S7BJBj~QxA;wRmx z;$)JfvZNX4I+)&XfE^ScNT6{rqP++iy<14QA1>^42b8;*LO%M`6rs(&>oIn+bb?*6 zeP>slN>=XLld?<}adEoQS^pJW{C&|ZRs#Eh)-f z(;r)@z9d?Gc}^;e>Ua|=#z@n(73Q-JsS_#F?CWXb#>J_-pbR_s6?t^Fm|%qDckP!i zsGv`%FoE&=J9#hqCpB(0u^#-G&vFtCmn{d31;tG{Z{rQA8{~~2?lwj`5F1Lk29iLD z8PK{f?j~mM<42U;3_Nzs%CBo|{(1^D*OvdZ^*mK=d4mx~(4CGmyL8RP4{cI%mHi)6 z<`*+nlkC|IyFG_H`(nhIB6rYjyUL?r literal 0 HcmV?d00001 diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index 90f6f49bd..220f10701 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -9,6 +9,7 @@ Highlighter.matchStudioSettings() local StringDiff = require(script:FindFirstChild("StringDiff")) local Timer = require(Plugin.Timer) +local Assets = require(Plugin.Assets) local Theme = require(Plugin.App.Theme) local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync) @@ -24,6 +25,7 @@ function StringDiffVisualizer:init() self.updateEvent = Instance.new("BindableEvent") self.lineHeight, self.setLineHeight = Roact.createBinding(15) self.canvasPosition, self.setCanvasPosition = Roact.createBinding(Vector2.zero) + self.windowWidth, self.setWindowWidth = Roact.createBinding(math.huge) -- Ensure that the script background is up to date with the current theme self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function() @@ -275,6 +277,34 @@ function StringDiffVisualizer:render() newDiffs = updatedNewDiffs end + -- Update the maxLines after we may have inserted new lines + maxLines = math.max(#richTextLinesOldString, #richTextLinesNewString) + + local removalScrollMarkers = {} + local insertionScrollMarkers = {} + for lineNum in oldDiffs do + table.insert( + removalScrollMarkers, + e("Frame", { + Size = UDim2.fromScale(0.5, 1 / maxLines), + Position = UDim2.fromScale(0, (lineNum - 1) / maxLines), + BorderSizePixel = 0, + BackgroundColor3 = theme.Diff.Remove, + }) + ) + end + for lineNum in newDiffs do + table.insert( + insertionScrollMarkers, + e("Frame", { + Size = UDim2.fromScale(0.5, 1 / maxLines), + Position = UDim2.fromScale(0.5, (lineNum - 1) / maxLines), + BorderSizePixel = 0, + BackgroundColor3 = theme.Diff.Add, + }) + ) + end + return e(BorderedContainer, { size = self.props.size, position = self.props.position, @@ -292,141 +322,161 @@ function StringDiffVisualizer:render() CornerRadius = UDim.new(0, 5), }), }), - Separator = e("Frame", { - Size = UDim2.new(0, 2, 1, 0), - Position = UDim2.new(0.5, 0, 0, 0), - AnchorPoint = Vector2.new(0.5, 0), - BorderSizePixel = 0, - BackgroundColor3 = theme.BorderedContainer.BorderColor, - BackgroundTransparency = 0.5, - }), - Old = e(VirtualScroller, { - position = UDim2.new(0, 2, 0, 2), - size = UDim2.new(0.5, -7, 1, -4), - transparency = self.props.transparency, - count = maxLines, - updateEvent = self.updateEvent.Event, - canvasWidth = canvasWidth, - canvasPosition = self.canvasPosition, - onCanvasPositionChanged = self.setCanvasPosition, - render = function(i) - if not richTextLinesOldString[i] then - return e("ImageLabel", { - Size = UDim2.fromScale(1, 1), - Position = UDim2.fromScale(0, 0), - BackgroundTransparency = 1, - BorderSizePixel = 0, - Image = "rbxassetid://117018699617466", - ImageTransparency = 0.7, - ImageColor3 = theme.TextColor, - ScaleType = Enum.ScaleType.Tile, - TileSize = UDim2.new(0, 64, 4, 0), - }) - end - - local lineDiffs = oldDiffs[i] - local diffFrames = table.create(if lineDiffs then #lineDiffs else 0) - - if lineDiffs then - local charWidth = math.round(theme.TextSize.Code * 0.5) - for diffIdx, diff in lineDiffs do - local start, stop = diff.start, diff.stop - diffFrames[diffIdx] = e("Frame", { - Size = UDim2.new(0, math.max(charWidth * (stop - start), charWidth * 0.4), 1, 0), - Position = UDim2.fromOffset(charWidth * start, 0), - BackgroundColor3 = theme.Diff.Remove, - BackgroundTransparency = 0.75, + Main = e("Frame", { + Size = UDim2.new(1, -10, 1, -2), + Position = UDim2.new(0, 2, 0, 2), + BackgroundTransparency = 1, + [Roact.Change.AbsoluteSize] = function(rbx) + self.setWindowWidth(rbx.AbsoluteSize.X * 0.5 - 10) + end, + }, { + Separator = e("Frame", { + Size = UDim2.new(0, 2, 1, 0), + Position = UDim2.new(0.5, 0, 0, 0), + AnchorPoint = Vector2.new(0.5, 0), + BorderSizePixel = 0, + BackgroundColor3 = theme.BorderedContainer.BorderColor, + BackgroundTransparency = 0.5, + }), + Old = e(VirtualScroller, { + position = UDim2.new(0, 0, 0, 0), + size = UDim2.new(0.5, -1, 1, 0), + transparency = self.props.transparency, + count = maxLines, + updateEvent = self.updateEvent.Event, + canvasWidth = canvasWidth, + canvasPosition = self.canvasPosition, + onCanvasPositionChanged = self.setCanvasPosition, + render = function(i) + if not richTextLinesOldString[i] then + return e("ImageLabel", { + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0, 0), + BackgroundTransparency = 1, BorderSizePixel = 0, - ZIndex = -1, + Image = Assets.Images.DiagonalLines, + ImageTransparency = 0.7, + ImageColor3 = theme.TextColor, + ScaleType = Enum.ScaleType.Tile, + TileSize = UDim2.new(0, 64, 4, 0), }) end - end - - return Roact.createFragment({ - CodeLabel = e("TextLabel", { - Size = UDim2.fromScale(1, 1), - Position = UDim2.fromScale(0, 0), - Text = richTextLinesOldString[i], - RichText = true, - BackgroundColor3 = theme.Diff.Remove, - BackgroundTransparency = if lineDiffs then 0.85 else 1, - BorderSizePixel = 0, - FontFace = theme.Font.Code, - TextSize = theme.TextSize.Code, - TextXAlignment = Enum.TextXAlignment.Left, - TextYAlignment = Enum.TextYAlignment.Top, - TextColor3 = Color3.fromRGB(255, 255, 255), - }), - DiffFrames = Roact.createFragment(diffFrames), - }) - end, - getHeightBinding = function() - return self.lineHeight - end, - }), - New = e(VirtualScroller, { - position = UDim2.new(0.5, 5, 0, 2), - size = UDim2.new(0.5, -7, 1, -4), - transparency = self.props.transparency, - count = maxLines, - updateEvent = self.updateEvent.Event, - canvasWidth = canvasWidth, - canvasPosition = self.canvasPosition, - onCanvasPositionChanged = self.setCanvasPosition, - render = function(i) - if not richTextLinesNewString[i] then - return e("ImageLabel", { - Size = UDim2.fromScale(1, 1), - Position = UDim2.fromScale(0, 0), - BackgroundTransparency = 1, - BorderSizePixel = 0, - Image = "rbxassetid://117018699617466", - ImageTransparency = 0.7, - ImageColor3 = theme.TextColor, - ScaleType = Enum.ScaleType.Tile, - TileSize = UDim2.new(0, 64, 4, 0), + + local lineDiffs = oldDiffs[i] + local diffFrames = table.create(if lineDiffs then #lineDiffs else 0) + + if lineDiffs then + local charWidth = math.round(theme.TextSize.Code * 0.5) + for diffIdx, diff in lineDiffs do + local start, stop = diff.start, diff.stop + diffFrames[diffIdx] = e("Frame", { + Size = UDim2.new(0, math.max(charWidth * (stop - start), charWidth * 0.4), 1, 0), + Position = UDim2.fromOffset(charWidth * start, 0), + BackgroundColor3 = theme.Diff.Remove, + BackgroundTransparency = 0.75, + BorderSizePixel = 0, + ZIndex = -1, + }) + end + end + + return Roact.createFragment({ + CodeLabel = e("TextLabel", { + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0, 0), + Text = richTextLinesOldString[i], + RichText = true, + BackgroundColor3 = theme.Diff.Remove, + BackgroundTransparency = if lineDiffs then 0.85 else 1, + BorderSizePixel = 0, + FontFace = theme.Font.Code, + TextSize = theme.TextSize.Code, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = Color3.fromRGB(255, 255, 255), + }), + DiffFrames = Roact.createFragment(diffFrames), }) - end - - local lineDiffs = newDiffs[i] - local diffFrames = table.create(if lineDiffs then #lineDiffs else 0) - - if lineDiffs then - local charWidth = math.round(theme.TextSize.Code * 0.5) - for diffIdx, diff in lineDiffs do - local start, stop = diff.start, diff.stop - diffFrames[diffIdx] = e("Frame", { - Size = UDim2.new(0, math.max(charWidth * (stop - start), charWidth * 0.4), 1, 0), - Position = UDim2.fromOffset(charWidth * start, 0), - BackgroundColor3 = theme.Diff.Add, - BackgroundTransparency = 0.75, + end, + getHeightBinding = function() + return self.lineHeight + end, + }), + New = e(VirtualScroller, { + position = UDim2.new(0.5, 1, 0, 0), + size = UDim2.new(0.5, -1, 1, 0), + transparency = self.props.transparency, + count = maxLines, + updateEvent = self.updateEvent.Event, + canvasWidth = canvasWidth, + canvasPosition = self.canvasPosition, + onCanvasPositionChanged = self.setCanvasPosition, + render = function(i) + if not richTextLinesNewString[i] then + return e("ImageLabel", { + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0, 0), + BackgroundTransparency = 1, BorderSizePixel = 0, - ZIndex = -1, + Image = Assets.Images.DiagonalLines, + ImageTransparency = 0.7, + ImageColor3 = theme.TextColor, + ScaleType = Enum.ScaleType.Tile, + TileSize = UDim2.new(0, 64, 4, 0), }) end - end - - return Roact.createFragment({ - CodeLabel = e("TextLabel", { - Size = UDim2.fromScale(1, 1), - Position = UDim2.fromScale(0, 0), - Text = richTextLinesNewString[i], - RichText = true, - BackgroundColor3 = theme.Diff.Add, - BackgroundTransparency = if lineDiffs then 0.85 else 1, - BorderSizePixel = 0, - FontFace = theme.Font.Code, - TextSize = theme.TextSize.Code, - TextXAlignment = Enum.TextXAlignment.Left, - TextYAlignment = Enum.TextYAlignment.Top, - TextColor3 = Color3.fromRGB(255, 255, 255), - }), - DiffFrames = Roact.createFragment(diffFrames), - }) - end, - getHeightBinding = function() - return self.lineHeight - end, + + local lineDiffs = newDiffs[i] + local diffFrames = table.create(if lineDiffs then #lineDiffs else 0) + + if lineDiffs then + local charWidth = math.round(theme.TextSize.Code * 0.5) + for diffIdx, diff in lineDiffs do + local start, stop = diff.start, diff.stop + diffFrames[diffIdx] = e("Frame", { + Size = UDim2.new(0, math.max(charWidth * (stop - start), charWidth * 0.4), 1, 0), + Position = UDim2.fromOffset(charWidth * start, 0), + BackgroundColor3 = theme.Diff.Add, + BackgroundTransparency = 0.75, + BorderSizePixel = 0, + ZIndex = -1, + }) + end + end + + return Roact.createFragment({ + CodeLabel = e("TextLabel", { + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0, 0), + Text = richTextLinesNewString[i], + RichText = true, + BackgroundColor3 = theme.Diff.Add, + BackgroundTransparency = if lineDiffs then 0.85 else 1, + BorderSizePixel = 0, + FontFace = theme.Font.Code, + TextSize = theme.TextSize.Code, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = Color3.fromRGB(255, 255, 255), + }), + DiffFrames = Roact.createFragment(diffFrames), + }) + end, + getHeightBinding = function() + return self.lineHeight + end, + }), + }), + ScrollMarkers = e("Frame", { + Size = self.windowWidth:map(function(windowWidth) + return UDim2.new(0, 8, 1, -4 - (if canvasWidth > windowWidth then 10 else 0)) + end), + Position = UDim2.new(1, -2, 0, 2), + AnchorPoint = Vector2.new(1, 0), + BackgroundTransparency = 1, + }, { + insertions = Roact.createFragment(insertionScrollMarkers), + removals = Roact.createFragment(removalScrollMarkers), }), }) end) diff --git a/plugin/src/Assets.lua b/plugin/src/Assets.lua index e48c1217e..73c208458 100644 --- a/plugin/src/Assets.lua +++ b/plugin/src/Assets.lua @@ -20,6 +20,7 @@ local Assets = { PluginButton = "rbxassetid://3405341609", PluginButtonConnected = "rbxassetid://9529783993", PluginButtonWarning = "rbxassetid://9529784530", + DiagonalLines = "rbxassetid://117018699617466", Icons = { Close = "rbxassetid://6012985953", Back = "rbxassetid://6017213752", From fb13c6a42f833769b8f04ad58df38d4a1aaa96d2 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Fri, 15 Nov 2024 17:43:00 -0800 Subject: [PATCH 09/17] Use current/incoming terminology --- .../Components/StringDiffVisualizer/init.lua | 228 +++++++++--------- plugin/src/App/StatusPages/Confirming.lua | 14 +- plugin/src/App/StatusPages/Connected.lua | 14 +- 3 files changed, 133 insertions(+), 123 deletions(-) diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index 220f10701..96fea38a3 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -40,10 +40,10 @@ function StringDiffVisualizer:init() self:updateScriptBackground() self:setState({ - oldDiffs = {}, - newDiffs = {}, - oldSpacers = {}, - newSpacers = {}, + currentDiffs = {}, + incomingDiffs = {}, + currentSpacers = {}, + incomingSpacers = {}, }) end @@ -60,44 +60,51 @@ function StringDiffVisualizer:updateScriptBackground() end function StringDiffVisualizer:didUpdate(previousProps) - if previousProps.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then + if + previousProps.currentString ~= self.props.currentString + or previousProps.incomingString ~= self.props.incomingString + then self:updateDiffs() end end function StringDiffVisualizer:calculateContentSize(theme) - local oldString, newString = self.props.oldString, self.props.newString + local currentString, incomingString = self.props.currentString, self.props.incomingString - local oldStringBounds = getTextBoundsAsync(oldString, theme.Font.Code, theme.TextSize.Code, math.huge) - local newStringBounds = getTextBoundsAsync(newString, theme.Font.Code, theme.TextSize.Code, math.huge) + local currentStringBounds = getTextBoundsAsync(currentString, theme.Font.Code, theme.TextSize.Code, math.huge) + local incomingStringBounds = getTextBoundsAsync(incomingString, theme.Font.Code, theme.TextSize.Code, math.huge) - return Vector2.new(math.max(oldStringBounds.X, newStringBounds.X), math.max(oldStringBounds.Y, newStringBounds.Y)) + return Vector2.new( + math.max(currentStringBounds.X, incomingStringBounds.X), + math.max(currentStringBounds.Y, incomingStringBounds.Y) + ) end function StringDiffVisualizer:updateDiffs() Timer.start("StringDiffVisualizer:updateDiffs") - local oldString, newString = self.props.oldString, self.props.newString + local currentString, incomingString = self.props.currentString, self.props.incomingString -- Diff the two texts local startClock = os.clock() - local diffs = StringDiff.findDiffs((string.gsub(oldString, "\t", " ")), (string.gsub(newString, "\t", " "))) + local diffs = + StringDiff.findDiffs((string.gsub(currentString, "\t", " ")), (string.gsub(incomingString, "\t", " "))) local stopClock = os.clock() Log.trace( "Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections", - #oldString, - #newString, + #currentString, + #incomingString, math.round((stopClock - startClock) * 1000 * 1000), #diffs ) -- Find the diff locations - local oldDiffs, newDiffs = {}, {} - local oldSpacers, newSpacers = {}, {} + local currentDiffs, incomingDiffs = {}, {} + local currentSpacers, incomingSpacers = {}, {} local firstDiffLineNum = 0 - local oldLineNum, oldIdx, newLineNum, newIdx = 1, 0, 1, 0 + local currentLineNum, currentIdx, incomingLineNum, incomingIdx = 1, 0, 1, 0 for _, diff in diffs do local actionType, text = diff.actionType, diff.value local lines = string.split(text, "\n") @@ -105,140 +112,143 @@ function StringDiffVisualizer:updateDiffs() if actionType == StringDiff.ActionTypes.Equal then for i, line in lines do if i > 1 then - oldLineNum += 1 - oldIdx = 0 - newLineNum += 1 - newIdx = 0 + currentLineNum += 1 + currentIdx = 0 + incomingLineNum += 1 + incomingIdx = 0 end - oldIdx += #line - newIdx += #line + currentIdx += #line + incomingIdx += #line end elseif actionType == StringDiff.ActionTypes.Insert then if firstDiffLineNum == 0 then - firstDiffLineNum = newLineNum + firstDiffLineNum = incomingLineNum end for i, line in lines do if i > 1 then - newLineNum += 1 - newIdx = 0 + incomingLineNum += 1 + incomingIdx = 0 - table.insert(oldSpacers, { oldLineNum = oldLineNum, newLineNum = newLineNum }) + table.insert(currentSpacers, { currentLineNum = currentLineNum, incomingLineNum = incomingLineNum }) end - if not newDiffs[newLineNum] then - newDiffs[newLineNum] = { - { start = newIdx, stop = newIdx + #line }, + if not incomingDiffs[incomingLineNum] then + incomingDiffs[incomingLineNum] = { + { start = incomingIdx, stop = incomingIdx + #line }, } else - table.insert(newDiffs[newLineNum], { - start = newIdx, - stop = newIdx + #line, + table.insert(incomingDiffs[incomingLineNum], { + start = incomingIdx, + stop = incomingIdx + #line, }) end - newIdx += #line + incomingIdx += #line end elseif actionType == StringDiff.ActionTypes.Delete then if firstDiffLineNum == 0 then - firstDiffLineNum = oldLineNum + firstDiffLineNum = currentLineNum end for i, line in lines do if i > 1 then - oldLineNum += 1 - oldIdx = 0 + currentLineNum += 1 + currentIdx = 0 - table.insert(newSpacers, { oldLineNum = oldLineNum, newLineNum = newLineNum }) + table.insert( + incomingSpacers, + { currentLineNum = currentLineNum, incomingLineNum = incomingLineNum } + ) end - if not oldDiffs[oldLineNum] then - oldDiffs[oldLineNum] = { - { start = oldIdx, stop = oldIdx + #line }, + if not currentDiffs[currentLineNum] then + currentDiffs[currentLineNum] = { + { start = currentIdx, stop = currentIdx + #line }, } else - table.insert(oldDiffs[oldLineNum], { - start = oldIdx, - stop = oldIdx + #line, + table.insert(currentDiffs[currentLineNum], { + start = currentIdx, + stop = currentIdx + #line, }) end - oldIdx += #line + currentIdx += #line end else Log.warn("Unknown diff action: {} {}", actionType, text) end end - -- Filter out diffs that are just newlines being added/removed from existing non-empty lines. + -- Filter out diffs that are just incominglines being added/removed from existing non-empty lines. -- This is done to make the diff visualization less noisy. - local oldStringLines = string.split(oldString, "\n") - local newStringLines = string.split(newString, "\n") + local currentStringLines = string.split(currentString, "\n") + local incomingStringLines = string.split(incomingString, "\n") - for lineNum, lineDiffs in oldDiffs do + for lineNum, lineDiffs in currentDiffs do if - (#lineDiffs > 1) -- Not just newline - or (lineDiffs[1].start ~= lineDiffs[1].stop) -- Not a newline at all - or (oldStringLines[lineNum] == "") -- Empty line, so the newline change is significant + (#lineDiffs > 1) -- Not just incomingline + or (lineDiffs[1].start ~= lineDiffs[1].stop) -- Not a incomingline at all + or (currentStringLines[lineNum] == "") -- Empty line, so the incomingline change is significant then continue end - -- Just a noisy newline diff, clear it - oldDiffs[lineNum] = nil + -- Just a noisy incomingline diff, clear it + currentDiffs[lineNum] = nil end - for lineNum, lineDiffs in newDiffs do + for lineNum, lineDiffs in incomingDiffs do if - (#lineDiffs > 1) -- Not just newline - or (lineDiffs[1].start ~= lineDiffs[1].stop) -- Not a newline at all - or (newStringLines[lineNum] == "") -- Empty line, so the newline change is significant + (#lineDiffs > 1) -- Not just incomingline + or (lineDiffs[1].start ~= lineDiffs[1].stop) -- Not a incomingline at all + or (incomingStringLines[lineNum] == "") -- Empty line, so the incomingline change is significant then continue end - -- Just a noisy newline diff, clear it - newDiffs[lineNum] = nil + -- Just a noisy incomingline diff, clear it + incomingDiffs[lineNum] = nil end Timer.stop() self:setState({ - oldDiffs = oldDiffs, - newDiffs = newDiffs, - oldSpacers = oldSpacers, - newSpacers = newSpacers, + currentDiffs = currentDiffs, + incomingDiffs = incomingDiffs, + currentSpacers = currentSpacers, + incomingSpacers = incomingSpacers, }) -- Scroll to the first diff line self.setCanvasPosition(Vector2.new(0, math.max(0, (firstDiffLineNum - 4) * 16))) end function StringDiffVisualizer:render() - local oldString, newString = self.props.oldString, self.props.newString - local oldDiffs, newDiffs = self.state.oldDiffs, self.state.newDiffs - local oldSpacers, newSpacers = self.state.oldSpacers, self.state.newSpacers + local currentString, incomingString = self.props.currentString, self.props.incomingString + local currentDiffs, incomingDiffs = self.state.currentDiffs, self.state.incomingDiffs + local currentSpacers, incomingSpacers = self.state.currentSpacers, self.state.incomingSpacers return Theme.with(function(theme) self.setLineHeight(theme.TextSize.Code) - local richTextLinesOldString = Highlighter.buildRichTextLines({ - src = oldString, + local richTextLinesCurrentString = Highlighter.buildRichTextLines({ + src = currentString, }) - local richTextLinesNewString = Highlighter.buildRichTextLines({ - src = newString, + local richTextLinesIncomingString = Highlighter.buildRichTextLines({ + src = incomingString, }) - local maxLines = math.max(#richTextLinesOldString, #richTextLinesNewString) + local maxLines = math.max(#richTextLinesCurrentString, #richTextLinesIncomingString) -- Calculate the width of the canvas -- (One line at a time to avoid the 200k char limit of getTextBoundsAsync) local canvasWidth = 0 for i = 1, maxLines do - local oldLine = richTextLinesOldString[i] - if oldLine and oldLine ~= "" then - local bounds = getTextBoundsAsync(oldLine, theme.Font.Code, theme.TextSize.Code, math.huge, true) + local currentLine = richTextLinesCurrentString[i] + if currentLine and currentLine ~= "" then + local bounds = getTextBoundsAsync(currentLine, theme.Font.Code, theme.TextSize.Code, math.huge, true) if bounds.X > canvasWidth then canvasWidth = bounds.X end end - local newLine = richTextLinesNewString[i] - if newLine and oldLine ~= "" then - local bounds = getTextBoundsAsync(newLine, theme.Font.Code, theme.TextSize.Code, math.huge, true) + local incomingLine = richTextLinesIncomingString[i] + if incomingLine and currentLine ~= "" then + local bounds = getTextBoundsAsync(incomingLine, theme.Font.Code, theme.TextSize.Code, math.huge, true) if bounds.X > canvasWidth then canvasWidth = bounds.X end @@ -246,43 +256,43 @@ function StringDiffVisualizer:render() end -- Adjust the rich text lines and their diffs to include spacers (aka nil lines) - for spacerIdx, spacer in oldSpacers do - local spacerLineNum = spacer.oldLineNum + (spacerIdx - 1) - table.insert(richTextLinesOldString, spacerLineNum, nil) - -- The oldDiffs that come after this spacer need to be moved down - -- without overwriting the oldDiffs that are already there - local updatedOldDiffs = {} - for lineNum, diffs in pairs(oldDiffs) do + for spacerIdx, spacer in currentSpacers do + local spacerLineNum = spacer.currentLineNum + (spacerIdx - 1) + table.insert(richTextLinesCurrentString, spacerLineNum, nil) + -- The currentDiffs that come after this spacer need to be moved down + -- without overwriting the currentDiffs that are already there + local updatedCurrentDiffs = {} + for lineNum, diffs in pairs(currentDiffs) do if lineNum >= spacerLineNum then - updatedOldDiffs[lineNum + 1] = diffs + updatedCurrentDiffs[lineNum + 1] = diffs else - updatedOldDiffs[lineNum] = diffs + updatedCurrentDiffs[lineNum] = diffs end end - oldDiffs = updatedOldDiffs + currentDiffs = updatedCurrentDiffs end - for spacerIdx, spacer in newSpacers do - local spacerLineNum = spacer.newLineNum + (spacerIdx - 1) - table.insert(richTextLinesNewString, spacerLineNum, nil) - -- The newDiffs that come after this spacer need to be moved down - -- without overwriting the newDiffs that are already there - local updatedNewDiffs = {} - for lineNum, diffs in pairs(newDiffs) do + for spacerIdx, spacer in incomingSpacers do + local spacerLineNum = spacer.incomingLineNum + (spacerIdx - 1) + table.insert(richTextLinesIncomingString, spacerLineNum, nil) + -- The incomingDiffs that come after this spacer need to be moved down + -- without overwriting the incomingDiffs that are already there + local updatedIncomingDiffs = {} + for lineNum, diffs in pairs(incomingDiffs) do if lineNum >= spacerLineNum then - updatedNewDiffs[lineNum + 1] = diffs + updatedIncomingDiffs[lineNum + 1] = diffs else - updatedNewDiffs[lineNum] = diffs + updatedIncomingDiffs[lineNum] = diffs end end - newDiffs = updatedNewDiffs + incomingDiffs = updatedIncomingDiffs end - -- Update the maxLines after we may have inserted new lines - maxLines = math.max(#richTextLinesOldString, #richTextLinesNewString) + -- Update the maxLines after we may have inserted additional lines + maxLines = math.max(#richTextLinesCurrentString, #richTextLinesIncomingString) local removalScrollMarkers = {} local insertionScrollMarkers = {} - for lineNum in oldDiffs do + for lineNum in currentDiffs do table.insert( removalScrollMarkers, e("Frame", { @@ -293,7 +303,7 @@ function StringDiffVisualizer:render() }) ) end - for lineNum in newDiffs do + for lineNum in incomingDiffs do table.insert( insertionScrollMarkers, e("Frame", { @@ -338,7 +348,7 @@ function StringDiffVisualizer:render() BackgroundColor3 = theme.BorderedContainer.BorderColor, BackgroundTransparency = 0.5, }), - Old = e(VirtualScroller, { + Current = e(VirtualScroller, { position = UDim2.new(0, 0, 0, 0), size = UDim2.new(0.5, -1, 1, 0), transparency = self.props.transparency, @@ -348,7 +358,7 @@ function StringDiffVisualizer:render() canvasPosition = self.canvasPosition, onCanvasPositionChanged = self.setCanvasPosition, render = function(i) - if not richTextLinesOldString[i] then + if not richTextLinesCurrentString[i] then return e("ImageLabel", { Size = UDim2.fromScale(1, 1), Position = UDim2.fromScale(0, 0), @@ -362,7 +372,7 @@ function StringDiffVisualizer:render() }) end - local lineDiffs = oldDiffs[i] + local lineDiffs = currentDiffs[i] local diffFrames = table.create(if lineDiffs then #lineDiffs else 0) if lineDiffs then @@ -384,7 +394,7 @@ function StringDiffVisualizer:render() CodeLabel = e("TextLabel", { Size = UDim2.fromScale(1, 1), Position = UDim2.fromScale(0, 0), - Text = richTextLinesOldString[i], + Text = richTextLinesCurrentString[i], RichText = true, BackgroundColor3 = theme.Diff.Remove, BackgroundTransparency = if lineDiffs then 0.85 else 1, @@ -402,7 +412,7 @@ function StringDiffVisualizer:render() return self.lineHeight end, }), - New = e(VirtualScroller, { + Incoming = e(VirtualScroller, { position = UDim2.new(0.5, 1, 0, 0), size = UDim2.new(0.5, -1, 1, 0), transparency = self.props.transparency, @@ -412,7 +422,7 @@ function StringDiffVisualizer:render() canvasPosition = self.canvasPosition, onCanvasPositionChanged = self.setCanvasPosition, render = function(i) - if not richTextLinesNewString[i] then + if not richTextLinesIncomingString[i] then return e("ImageLabel", { Size = UDim2.fromScale(1, 1), Position = UDim2.fromScale(0, 0), @@ -426,7 +436,7 @@ function StringDiffVisualizer:render() }) end - local lineDiffs = newDiffs[i] + local lineDiffs = incomingDiffs[i] local diffFrames = table.create(if lineDiffs then #lineDiffs else 0) if lineDiffs then @@ -448,7 +458,7 @@ function StringDiffVisualizer:render() CodeLabel = e("TextLabel", { Size = UDim2.fromScale(1, 1), Position = UDim2.fromScale(0, 0), - Text = richTextLinesNewString[i], + Text = richTextLinesIncomingString[i], RichText = true, BackgroundColor3 = theme.Diff.Add, BackgroundTransparency = if lineDiffs then 0.85 else 1, diff --git a/plugin/src/App/StatusPages/Confirming.lua b/plugin/src/App/StatusPages/Confirming.lua index 3f3958a4c..b7f65d2b7 100644 --- a/plugin/src/App/StatusPages/Confirming.lua +++ b/plugin/src/App/StatusPages/Confirming.lua @@ -26,8 +26,8 @@ function ConfirmingPage:init() self:setState({ patchTree = nil, showingStringDiff = false, - oldString = "", - newString = "", + currentString = "", + incomingString = "", showingTableDiff = false, oldTable = {}, newTable = {}, @@ -81,11 +81,11 @@ function ConfirmingPage:render() patchTree = self.state.patchTree, - showStringDiff = function(oldString: string, newString: string) + showStringDiff = function(currentString: string, incomingString: string) self:setState({ showingStringDiff = true, - oldString = oldString, - newString = newString, + currentString = currentString, + incomingString = incomingString, }) end, showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? }) @@ -192,8 +192,8 @@ function ConfirmingPage:render() anchorPoint = Vector2.new(0, 0), transparency = self.props.transparency, - oldString = self.state.oldString, - newString = self.state.newString, + currentString = self.state.currentString, + incomingString = self.state.incomingString, }), }), }), diff --git a/plugin/src/App/StatusPages/Connected.lua b/plugin/src/App/StatusPages/Connected.lua index 1d089e28f..2a489366c 100644 --- a/plugin/src/App/StatusPages/Connected.lua +++ b/plugin/src/App/StatusPages/Connected.lua @@ -307,8 +307,8 @@ function ConnectedPage:init() renderChanges = false, hoveringChangeInfo = false, showingStringDiff = false, - oldString = "", - newString = "", + currentString = "", + incomingString = "", }) self.changeInfoText, self.setChangeInfoText = Roact.createBinding("") @@ -511,11 +511,11 @@ function ConnectedPage:render() patchData = self.props.patchData, patchTree = self.props.patchTree, serveSession = self.props.serveSession, - showStringDiff = function(oldString: string, newString: string) + showStringDiff = function(currentString: string, incomingString: string) self:setState({ showingStringDiff = true, - oldString = oldString, - newString = newString, + currentString = currentString, + incomingString = incomingString, }) end, showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? }) @@ -566,8 +566,8 @@ function ConnectedPage:render() anchorPoint = Vector2.new(0, 0), transparency = self.props.transparency, - oldString = self.state.oldString, - newString = self.state.newString, + currentString = self.state.currentString, + incomingString = self.state.incomingString, }), }), }), From 21828fc65227706a6b8268525847939430d475e6 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Fri, 15 Nov 2024 18:32:47 -0800 Subject: [PATCH 10/17] More progress --- .../Components/StringDiffVisualizer/init.lua | 225 ++++++++++++------ plugin/src/App/Components/VirtualScroller.lua | 4 +- 2 files changed, 149 insertions(+), 80 deletions(-) diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index 96fea38a3..de1d02c0c 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -40,10 +40,13 @@ function StringDiffVisualizer:init() self:updateScriptBackground() self:setState({ + maxLines = 0, + currentRichTextLines = {}, + incomingRichTextLines = {}, currentDiffs = {}, + currentLineNumbers = {}, incomingDiffs = {}, - currentSpacers = {}, - incomingSpacers = {}, + incomingLineNumbers = {}, }) end @@ -206,48 +209,102 @@ function StringDiffVisualizer:updateDiffs() incomingDiffs[lineNum] = nil end + local currentRichTextLines = Highlighter.buildRichTextLines({ + src = currentString, + }) + local incomingRichTextLines = Highlighter.buildRichTextLines({ + src = incomingString, + }) + + local maxLines = math.max(#currentRichTextLines, #incomingRichTextLines) + + -- Adjust the rich text lines and their diffs to include spacers (aka nil lines) + for spacerIdx, spacer in currentSpacers do + local spacerLineNum = spacer.currentLineNum + (spacerIdx - 1) + table.insert(currentRichTextLines, spacerLineNum, nil) + -- The currentDiffs that come after this spacer need to be moved down + -- without overwriting the currentDiffs that are already there + local updatedCurrentDiffs = {} + for lineNum, lineDiffs in pairs(currentDiffs) do + if lineNum >= spacerLineNum then + updatedCurrentDiffs[lineNum + 1] = lineDiffs + else + updatedCurrentDiffs[lineNum] = lineDiffs + end + end + currentDiffs = updatedCurrentDiffs + end + for spacerIdx, spacer in incomingSpacers do + local spacerLineNum = spacer.incomingLineNum + (spacerIdx - 1) + table.insert(incomingRichTextLines, spacerLineNum, nil) + -- The incomingDiffs that come after this spacer need to be moved down + -- without overwriting the incomingDiffs that are already there + local updatedIncomingDiffs = {} + for lineNum, lineDiffs in pairs(incomingDiffs) do + if lineNum >= spacerLineNum then + updatedIncomingDiffs[lineNum + 1] = lineDiffs + else + updatedIncomingDiffs[lineNum] = lineDiffs + end + end + incomingDiffs = updatedIncomingDiffs + end + + -- Update the maxLines after we may have inserted additional lines + maxLines = math.max(#currentRichTextLines, #incomingRichTextLines) + + local currentLineNumbers, incomingLineNumbers = table.create(maxLines, 0), table.create(maxLines, 0) + local currentLineNumber, incomingLineNumber = 0, 0 + for lineNum = 1, maxLines do + if currentRichTextLines[lineNum] then + currentLineNumber += 1 + currentLineNumbers[lineNum] = currentLineNumber + end + if incomingRichTextLines[lineNum] then + incomingLineNumber += 1 + incomingLineNumbers[lineNum] = incomingLineNumber + end + end + Timer.stop() self:setState({ + maxLines = maxLines, + currentRichTextLines = currentRichTextLines, + incomingRichTextLines = incomingRichTextLines, currentDiffs = currentDiffs, + currentLineNumbers = currentLineNumbers, incomingDiffs = incomingDiffs, - currentSpacers = currentSpacers, - incomingSpacers = incomingSpacers, + incomingLineNumbers = incomingLineNumbers, }) + -- Scroll to the first diff line - self.setCanvasPosition(Vector2.new(0, math.max(0, (firstDiffLineNum - 4) * 16))) + task.defer(self.setCanvasPosition, Vector2.new(0, math.max(0, (firstDiffLineNum - 4) * 16))) end function StringDiffVisualizer:render() - local currentString, incomingString = self.props.currentString, self.props.incomingString local currentDiffs, incomingDiffs = self.state.currentDiffs, self.state.incomingDiffs - local currentSpacers, incomingSpacers = self.state.currentSpacers, self.state.incomingSpacers + local currentLineNumbers, incomingLineNumbers = self.state.currentLineNumbers, self.state.incomingLineNumbers + local currentRichTextLines, incomingRichTextLines = + self.state.currentRichTextLines, self.state.incomingRichTextLines + local maxLines = self.state.maxLines return Theme.with(function(theme) self.setLineHeight(theme.TextSize.Code) - local richTextLinesCurrentString = Highlighter.buildRichTextLines({ - src = currentString, - }) - local richTextLinesIncomingString = Highlighter.buildRichTextLines({ - src = incomingString, - }) - - local maxLines = math.max(#richTextLinesCurrentString, #richTextLinesIncomingString) - -- Calculate the width of the canvas -- (One line at a time to avoid the 200k char limit of getTextBoundsAsync) local canvasWidth = 0 for i = 1, maxLines do - local currentLine = richTextLinesCurrentString[i] - if currentLine and currentLine ~= "" then + local currentLine = currentRichTextLines[i] + if currentLine and string.find(currentLine, "%S") then local bounds = getTextBoundsAsync(currentLine, theme.Font.Code, theme.TextSize.Code, math.huge, true) if bounds.X > canvasWidth then canvasWidth = bounds.X end end - local incomingLine = richTextLinesIncomingString[i] - if incomingLine and currentLine ~= "" then + local incomingLine = incomingRichTextLines[i] + if incomingLine and string.find(incomingLine, "%S") then local bounds = getTextBoundsAsync(incomingLine, theme.Font.Code, theme.TextSize.Code, math.huge, true) if bounds.X > canvasWidth then canvasWidth = bounds.X @@ -255,40 +312,10 @@ function StringDiffVisualizer:render() end end - -- Adjust the rich text lines and their diffs to include spacers (aka nil lines) - for spacerIdx, spacer in currentSpacers do - local spacerLineNum = spacer.currentLineNum + (spacerIdx - 1) - table.insert(richTextLinesCurrentString, spacerLineNum, nil) - -- The currentDiffs that come after this spacer need to be moved down - -- without overwriting the currentDiffs that are already there - local updatedCurrentDiffs = {} - for lineNum, diffs in pairs(currentDiffs) do - if lineNum >= spacerLineNum then - updatedCurrentDiffs[lineNum + 1] = diffs - else - updatedCurrentDiffs[lineNum] = diffs - end - end - currentDiffs = updatedCurrentDiffs - end - for spacerIdx, spacer in incomingSpacers do - local spacerLineNum = spacer.incomingLineNum + (spacerIdx - 1) - table.insert(richTextLinesIncomingString, spacerLineNum, nil) - -- The incomingDiffs that come after this spacer need to be moved down - -- without overwriting the incomingDiffs that are already there - local updatedIncomingDiffs = {} - for lineNum, diffs in pairs(incomingDiffs) do - if lineNum >= spacerLineNum then - updatedIncomingDiffs[lineNum + 1] = diffs - else - updatedIncomingDiffs[lineNum] = diffs - end - end - incomingDiffs = updatedIncomingDiffs - end + local lineNumberWidth = + getTextBoundsAsync(tostring(maxLines), theme.Font.Code, theme.TextSize.Body, math.huge, true).X - -- Update the maxLines after we may have inserted additional lines - maxLines = math.max(#richTextLinesCurrentString, #richTextLinesIncomingString) + canvasWidth += lineNumberWidth + 12 local removalScrollMarkers = {} local insertionScrollMarkers = {} @@ -358,7 +385,7 @@ function StringDiffVisualizer:render() canvasPosition = self.canvasPosition, onCanvasPositionChanged = self.setCanvasPosition, render = function(i) - if not richTextLinesCurrentString[i] then + if not currentRichTextLines[i] then return e("ImageLabel", { Size = UDim2.fromScale(1, 1), Position = UDim2.fromScale(0, 0), @@ -391,21 +418,41 @@ function StringDiffVisualizer:render() end return Roact.createFragment({ - CodeLabel = e("TextLabel", { - Size = UDim2.fromScale(1, 1), - Position = UDim2.fromScale(0, 0), - Text = richTextLinesCurrentString[i], - RichText = true, - BackgroundColor3 = theme.Diff.Remove, - BackgroundTransparency = if lineDiffs then 0.85 else 1, + LineNumber = e("TextLabel", { + Size = UDim2.new(0, lineNumberWidth + 8, 1, 0), + Text = currentLineNumbers[i] or "", + BackgroundColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 0.9, BorderSizePixel = 0, FontFace = theme.Font.Code, - TextSize = theme.TextSize.Code, - TextXAlignment = Enum.TextXAlignment.Left, - TextYAlignment = Enum.TextYAlignment.Top, - TextColor3 = Color3.fromRGB(255, 255, 255), + TextSize = theme.TextSize.Body, + TextColor3 = if lineDiffs then theme.Diff.Remove else theme.SubTextColor, + TextXAlignment = Enum.TextXAlignment.Right, + }, { + Padding = e("UIPadding", { PaddingRight = UDim.new(0, 6) }), + }), + Content = e("Frame", { + Size = UDim2.new(1, -(lineNumberWidth + 10), 1, 0), + Position = UDim2.fromScale(1, 0), + AnchorPoint = Vector2.new(1, 0), + BackgroundTransparency = 1, + }, { + CodeLabel = e("TextLabel", { + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0, 0), + Text = currentRichTextLines[i], + RichText = true, + BackgroundColor3 = theme.Diff.Remove, + BackgroundTransparency = if lineDiffs then 0.85 else 1, + BorderSizePixel = 0, + FontFace = theme.Font.Code, + TextSize = theme.TextSize.Code, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = Color3.fromRGB(255, 255, 255), + }), + DiffFrames = Roact.createFragment(diffFrames), }), - DiffFrames = Roact.createFragment(diffFrames), }) end, getHeightBinding = function() @@ -422,7 +469,7 @@ function StringDiffVisualizer:render() canvasPosition = self.canvasPosition, onCanvasPositionChanged = self.setCanvasPosition, render = function(i) - if not richTextLinesIncomingString[i] then + if not incomingRichTextLines[i] then return e("ImageLabel", { Size = UDim2.fromScale(1, 1), Position = UDim2.fromScale(0, 0), @@ -455,21 +502,41 @@ function StringDiffVisualizer:render() end return Roact.createFragment({ - CodeLabel = e("TextLabel", { - Size = UDim2.fromScale(1, 1), - Position = UDim2.fromScale(0, 0), - Text = richTextLinesIncomingString[i], - RichText = true, - BackgroundColor3 = theme.Diff.Add, - BackgroundTransparency = if lineDiffs then 0.85 else 1, + LineNumber = e("TextLabel", { + Size = UDim2.new(0, lineNumberWidth + 8, 1, 0), + Text = incomingLineNumbers[i] or "", + BackgroundColor3 = Color3.new(0, 0, 0), + BackgroundTransparency = 0.9, BorderSizePixel = 0, FontFace = theme.Font.Code, - TextSize = theme.TextSize.Code, - TextXAlignment = Enum.TextXAlignment.Left, - TextYAlignment = Enum.TextYAlignment.Top, - TextColor3 = Color3.fromRGB(255, 255, 255), + TextSize = theme.TextSize.Body, + TextColor3 = if lineDiffs then theme.Diff.Add else theme.SubTextColor, + TextXAlignment = Enum.TextXAlignment.Right, + }, { + Padding = e("UIPadding", { PaddingRight = UDim.new(0, 6) }), + }), + Content = e("Frame", { + Size = UDim2.new(1, -(lineNumberWidth + 10), 1, 0), + Position = UDim2.fromScale(1, 0), + AnchorPoint = Vector2.new(1, 0), + BackgroundTransparency = 1, + }, { + CodeLabel = e("TextLabel", { + Size = UDim2.fromScale(1, 1), + Position = UDim2.fromScale(0, 0), + Text = incomingRichTextLines[i], + RichText = true, + BackgroundColor3 = theme.Diff.Add, + BackgroundTransparency = if lineDiffs then 0.85 else 1, + BorderSizePixel = 0, + FontFace = theme.Font.Code, + TextSize = theme.TextSize.Code, + TextXAlignment = Enum.TextXAlignment.Left, + TextYAlignment = Enum.TextYAlignment.Top, + TextColor3 = Color3.fromRGB(255, 255, 255), + }), + DiffFrames = Roact.createFragment(diffFrames), }), - DiffFrames = Roact.createFragment(diffFrames), }) end, getHeightBinding = function() diff --git a/plugin/src/App/Components/VirtualScroller.lua b/plugin/src/App/Components/VirtualScroller.lua index a60cd69c8..63d966ca8 100644 --- a/plugin/src/App/Components/VirtualScroller.lua +++ b/plugin/src/App/Components/VirtualScroller.lua @@ -16,7 +16,9 @@ function VirtualScroller:init() self.scrollFrameRef = Roact.createRef() self:setState({ WindowSize = Vector2.zero, - CanvasPosition = Vector2.zero, + CanvasPosition = if self.props.canvasPosition + then self.props.canvasPosition:getValue() or Vector2.zero + else Vector2.zero, }) self.totalCanvas, self.setTotalCanvas = Roact.createBinding(0) From 1e2a0cfba9bd7441fa98d9a4304f6d70afbe5c5c Mon Sep 17 00:00:00 2001 From: boatbomber Date: Fri, 15 Nov 2024 18:40:45 -0800 Subject: [PATCH 11/17] Fix spacers offset --- plugin/src/App/Components/StringDiffVisualizer/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index de1d02c0c..4c49a30a6 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -220,7 +220,7 @@ function StringDiffVisualizer:updateDiffs() -- Adjust the rich text lines and their diffs to include spacers (aka nil lines) for spacerIdx, spacer in currentSpacers do - local spacerLineNum = spacer.currentLineNum + (spacerIdx - 1) + local spacerLineNum = spacer.currentLineNum + spacerIdx table.insert(currentRichTextLines, spacerLineNum, nil) -- The currentDiffs that come after this spacer need to be moved down -- without overwriting the currentDiffs that are already there @@ -235,7 +235,7 @@ function StringDiffVisualizer:updateDiffs() currentDiffs = updatedCurrentDiffs end for spacerIdx, spacer in incomingSpacers do - local spacerLineNum = spacer.incomingLineNum + (spacerIdx - 1) + local spacerLineNum = spacer.incomingLineNum + spacerIdx table.insert(incomingRichTextLines, spacerLineNum, nil) -- The incomingDiffs that come after this spacer need to be moved down -- without overwriting the incomingDiffs that are already there From a22fdd887474d90b10e156bb1ece0959d21663df Mon Sep 17 00:00:00 2001 From: boatbomber Date: Fri, 15 Nov 2024 18:44:59 -0800 Subject: [PATCH 12/17] Revert "Fix spacers offset" This reverts commit 1e2a0cfba9bd7441fa98d9a4304f6d70afbe5c5c. --- plugin/src/App/Components/StringDiffVisualizer/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index 4c49a30a6..de1d02c0c 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -220,7 +220,7 @@ function StringDiffVisualizer:updateDiffs() -- Adjust the rich text lines and their diffs to include spacers (aka nil lines) for spacerIdx, spacer in currentSpacers do - local spacerLineNum = spacer.currentLineNum + spacerIdx + local spacerLineNum = spacer.currentLineNum + (spacerIdx - 1) table.insert(currentRichTextLines, spacerLineNum, nil) -- The currentDiffs that come after this spacer need to be moved down -- without overwriting the currentDiffs that are already there @@ -235,7 +235,7 @@ function StringDiffVisualizer:updateDiffs() currentDiffs = updatedCurrentDiffs end for spacerIdx, spacer in incomingSpacers do - local spacerLineNum = spacer.incomingLineNum + spacerIdx + local spacerLineNum = spacer.incomingLineNum + (spacerIdx - 1) table.insert(incomingRichTextLines, spacerLineNum, nil) -- The incomingDiffs that come after this spacer need to be moved down -- without overwriting the incomingDiffs that are already there From 4b103a01be78ef3cb169a70ba5bf1f60a0af424c Mon Sep 17 00:00:00 2001 From: boatbomber Date: Fri, 15 Nov 2024 18:47:54 -0800 Subject: [PATCH 13/17] Reapply "Fix spacers offset" This reverts commit a22fdd887474d90b10e156bb1ece0959d21663df. --- plugin/src/App/Components/StringDiffVisualizer/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index de1d02c0c..4c49a30a6 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -220,7 +220,7 @@ function StringDiffVisualizer:updateDiffs() -- Adjust the rich text lines and their diffs to include spacers (aka nil lines) for spacerIdx, spacer in currentSpacers do - local spacerLineNum = spacer.currentLineNum + (spacerIdx - 1) + local spacerLineNum = spacer.currentLineNum + spacerIdx table.insert(currentRichTextLines, spacerLineNum, nil) -- The currentDiffs that come after this spacer need to be moved down -- without overwriting the currentDiffs that are already there @@ -235,7 +235,7 @@ function StringDiffVisualizer:updateDiffs() currentDiffs = updatedCurrentDiffs end for spacerIdx, spacer in incomingSpacers do - local spacerLineNum = spacer.incomingLineNum + (spacerIdx - 1) + local spacerLineNum = spacer.incomingLineNum + spacerIdx table.insert(incomingRichTextLines, spacerLineNum, nil) -- The incomingDiffs that come after this spacer need to be moved down -- without overwriting the incomingDiffs that are already there From 7584e7251ebf1f12e0acfc64198c823d72f317ba Mon Sep 17 00:00:00 2001 From: boatbomber Date: Fri, 15 Nov 2024 18:59:55 -0800 Subject: [PATCH 14/17] lol bad find&replace --- .../Components/StringDiffVisualizer/init.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index 4c49a30a6..446d6d25a 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -179,7 +179,7 @@ function StringDiffVisualizer:updateDiffs() end end - -- Filter out diffs that are just incominglines being added/removed from existing non-empty lines. + -- Filter out diffs that are just newlines being added/removed from existing non-empty lines. -- This is done to make the diff visualization less noisy. local currentStringLines = string.split(currentString, "\n") @@ -187,25 +187,25 @@ function StringDiffVisualizer:updateDiffs() for lineNum, lineDiffs in currentDiffs do if - (#lineDiffs > 1) -- Not just incomingline - or (lineDiffs[1].start ~= lineDiffs[1].stop) -- Not a incomingline at all - or (currentStringLines[lineNum] == "") -- Empty line, so the incomingline change is significant + (#lineDiffs > 1) -- Not just newline + or (lineDiffs[1].start ~= lineDiffs[1].stop) -- Not a newline at all + or (currentStringLines[lineNum] == "") -- Empty line, so the newline change is significant then continue end - -- Just a noisy incomingline diff, clear it + -- Just a noisy newline diff, clear it currentDiffs[lineNum] = nil end for lineNum, lineDiffs in incomingDiffs do if - (#lineDiffs > 1) -- Not just incomingline - or (lineDiffs[1].start ~= lineDiffs[1].stop) -- Not a incomingline at all - or (incomingStringLines[lineNum] == "") -- Empty line, so the incomingline change is significant + (#lineDiffs > 1) -- Not just newline + or (lineDiffs[1].start ~= lineDiffs[1].stop) -- Not a newline at all + or (incomingStringLines[lineNum] == "") -- Empty line, so the newline change is significant then continue end - -- Just a noisy incomingline diff, clear it + -- Just a noisy newline diff, clear it incomingDiffs[lineNum] = nil end From 7ba6897a1ccfd7f0db51d3e2b7725d9d36323b98 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 24 Dec 2024 01:17:11 -0800 Subject: [PATCH 15/17] Add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 088a96697..a4ffeacd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * Added popout diff visualizer for table properties like Attributes and Tags ([#834]) * Updated Theme to use Studio colors ([#838]) * Improved patch visualizer UX ([#883]) +* Improved script source diff visualizer UX and performance ([#994]) * Added update notifications for newer compatible versions in the Studio plugin. ([#832]) * Added experimental setting for Auto Connect in playtests ([#840]) * Improved settings UI ([#886]) @@ -91,6 +92,7 @@ [#974]: https://github.com/rojo-rbx/rojo/pull/974 [#987]: https://github.com/rojo-rbx/rojo/pull/987 [#988]: https://github.com/rojo-rbx/rojo/pull/988 +[#994]: https://github.com/rojo-rbx/rojo/pull/994 ## [7.4.3] - August 6th, 2024 * Fixed issue with building binary files introduced in 7.4.2 From bcd9abc92a220e5e07bae3e0a17c7fce5ea196a2 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 24 Dec 2024 01:29:47 -0800 Subject: [PATCH 16/17] Less harsh backgrounds --- plugin/src/App/Components/StringDiffVisualizer/init.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index 446d6d25a..a7e76c1ee 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -410,7 +410,7 @@ function StringDiffVisualizer:render() Size = UDim2.new(0, math.max(charWidth * (stop - start), charWidth * 0.4), 1, 0), Position = UDim2.fromOffset(charWidth * start, 0), BackgroundColor3 = theme.Diff.Remove, - BackgroundTransparency = 0.75, + BackgroundTransparency = 0.85, BorderSizePixel = 0, ZIndex = -1, }) @@ -443,7 +443,7 @@ function StringDiffVisualizer:render() Text = currentRichTextLines[i], RichText = true, BackgroundColor3 = theme.Diff.Remove, - BackgroundTransparency = if lineDiffs then 0.85 else 1, + BackgroundTransparency = if lineDiffs then 0.95 else 1, BorderSizePixel = 0, FontFace = theme.Font.Code, TextSize = theme.TextSize.Code, @@ -494,7 +494,7 @@ function StringDiffVisualizer:render() Size = UDim2.new(0, math.max(charWidth * (stop - start), charWidth * 0.4), 1, 0), Position = UDim2.fromOffset(charWidth * start, 0), BackgroundColor3 = theme.Diff.Add, - BackgroundTransparency = 0.75, + BackgroundTransparency = 0.85, BorderSizePixel = 0, ZIndex = -1, }) @@ -527,7 +527,7 @@ function StringDiffVisualizer:render() Text = incomingRichTextLines[i], RichText = true, BackgroundColor3 = theme.Diff.Add, - BackgroundTransparency = if lineDiffs then 0.85 else 1, + BackgroundTransparency = if lineDiffs then 0.95 else 1, BorderSizePixel = 0, FontFace = theme.Font.Code, TextSize = theme.TextSize.Code, From 886706031a29f8e2cdd143c766a80160dbd2929c Mon Sep 17 00:00:00 2001 From: boatbomber Date: Tue, 24 Dec 2024 01:57:41 -0800 Subject: [PATCH 17/17] Using nil is behaving weirdly --- .../Components/StringDiffVisualizer/init.lua | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index a7e76c1ee..4710842ce 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -101,6 +101,16 @@ function StringDiffVisualizer:updateDiffs() #diffs ) + -- Build the rich text lines + local currentRichTextLines = Highlighter.buildRichTextLines({ + src = currentString, + }) + local incomingRichTextLines = Highlighter.buildRichTextLines({ + src = incomingString, + }) + + local maxLines = math.max(#currentRichTextLines, #incomingRichTextLines) + -- Find the diff locations local currentDiffs, incomingDiffs = {}, {} local currentSpacers, incomingSpacers = {}, {} @@ -209,19 +219,10 @@ function StringDiffVisualizer:updateDiffs() incomingDiffs[lineNum] = nil end - local currentRichTextLines = Highlighter.buildRichTextLines({ - src = currentString, - }) - local incomingRichTextLines = Highlighter.buildRichTextLines({ - src = incomingString, - }) - - local maxLines = math.max(#currentRichTextLines, #incomingRichTextLines) - -- Adjust the rich text lines and their diffs to include spacers (aka nil lines) for spacerIdx, spacer in currentSpacers do - local spacerLineNum = spacer.currentLineNum + spacerIdx - table.insert(currentRichTextLines, spacerLineNum, nil) + local spacerLineNum = spacer.currentLineNum + spacerIdx - 1 + table.insert(currentRichTextLines, spacerLineNum, 0) -- The currentDiffs that come after this spacer need to be moved down -- without overwriting the currentDiffs that are already there local updatedCurrentDiffs = {} @@ -235,8 +236,9 @@ function StringDiffVisualizer:updateDiffs() currentDiffs = updatedCurrentDiffs end for spacerIdx, spacer in incomingSpacers do - local spacerLineNum = spacer.incomingLineNum + spacerIdx - table.insert(incomingRichTextLines, spacerLineNum, nil) + local spacerLineNum = spacer.incomingLineNum + spacerIdx - 1 + table.insert(incomingRichTextLines, spacerLineNum, 0) + -- The incomingDiffs that come after this spacer need to be moved down -- without overwriting the incomingDiffs that are already there local updatedIncomingDiffs = {} @@ -256,11 +258,11 @@ function StringDiffVisualizer:updateDiffs() local currentLineNumbers, incomingLineNumbers = table.create(maxLines, 0), table.create(maxLines, 0) local currentLineNumber, incomingLineNumber = 0, 0 for lineNum = 1, maxLines do - if currentRichTextLines[lineNum] then + if type(currentRichTextLines[lineNum]) == "string" then currentLineNumber += 1 currentLineNumbers[lineNum] = currentLineNumber end - if incomingRichTextLines[lineNum] then + if type(incomingRichTextLines[lineNum]) == "string" then incomingLineNumber += 1 incomingLineNumbers[lineNum] = incomingLineNumber end @@ -297,14 +299,14 @@ function StringDiffVisualizer:render() local canvasWidth = 0 for i = 1, maxLines do local currentLine = currentRichTextLines[i] - if currentLine and string.find(currentLine, "%S") then + if type(currentLine) == "string" and string.find(currentLine, "%S") then local bounds = getTextBoundsAsync(currentLine, theme.Font.Code, theme.TextSize.Code, math.huge, true) if bounds.X > canvasWidth then canvasWidth = bounds.X end end local incomingLine = incomingRichTextLines[i] - if incomingLine and string.find(incomingLine, "%S") then + if type(incomingLine) == "string" and string.find(incomingLine, "%S") then local bounds = getTextBoundsAsync(incomingLine, theme.Font.Code, theme.TextSize.Code, math.huge, true) if bounds.X > canvasWidth then canvasWidth = bounds.X @@ -385,7 +387,7 @@ function StringDiffVisualizer:render() canvasPosition = self.canvasPosition, onCanvasPositionChanged = self.setCanvasPosition, render = function(i) - if not currentRichTextLines[i] then + if type(currentRichTextLines[i]) ~= "string" then return e("ImageLabel", { Size = UDim2.fromScale(1, 1), Position = UDim2.fromScale(0, 0), @@ -469,7 +471,7 @@ function StringDiffVisualizer:render() canvasPosition = self.canvasPosition, onCanvasPositionChanged = self.setCanvasPosition, render = function(i) - if not incomingRichTextLines[i] then + if type(incomingRichTextLines[i]) ~= "string" then return e("ImageLabel", { Size = UDim2.fromScale(1, 1), Position = UDim2.fromScale(0, 0),