forked from rojo-rbx/rojo
453 lines
14 KiB
Lua
453 lines
14 KiB
Lua
local Rojo = script:FindFirstAncestor("Rojo")
|
|
local Plugin = Rojo.Plugin
|
|
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 BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
|
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
|
|
|
|
local e = Roact.createElement
|
|
|
|
local StringDiffVisualizer = Roact.Component:extend("StringDiffVisualizer")
|
|
|
|
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)
|
|
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()
|
|
-- Delay to allow Highlighter to process the theme change first
|
|
task.delay(1 / 20, function()
|
|
self:updateScriptBackground()
|
|
self:updateDiffs()
|
|
-- Rerender the virtual list elements
|
|
self.updateEvent:Fire()
|
|
end)
|
|
end)
|
|
|
|
self:updateScriptBackground()
|
|
self:updateDiffs()
|
|
end
|
|
|
|
function StringDiffVisualizer:willUnmount()
|
|
self.themeChangedConnection:Disconnect()
|
|
self.updateEvent:Destroy()
|
|
end
|
|
|
|
function StringDiffVisualizer:updateScriptBackground()
|
|
local backgroundColor = Highlighter.getTokenColor("background")
|
|
if backgroundColor ~= self.scriptBackground:getValue() then
|
|
self.setScriptBackground(backgroundColor)
|
|
end
|
|
end
|
|
|
|
function StringDiffVisualizer:didUpdate(previousProps)
|
|
if
|
|
previousProps.currentString ~= self.props.currentString
|
|
or previousProps.incomingString ~= self.props.incomingString
|
|
then
|
|
self:updateDiffs()
|
|
end
|
|
end
|
|
|
|
function StringDiffVisualizer:updateDiffs()
|
|
Timer.start("StringDiffVisualizer:updateDiffs")
|
|
local currentString, incomingString = self.props.currentString, self.props.incomingString
|
|
|
|
-- Diff the two texts
|
|
local startClock = os.clock()
|
|
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",
|
|
#currentString,
|
|
#incomingString,
|
|
math.round((stopClock - startClock) * 1000 * 1000),
|
|
#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 firstDiffLineNum = 0
|
|
|
|
local currentLineNum, incomingLineNum = 1, 1
|
|
local currentIdx, incomingIdx = 1, 1
|
|
for _, diff in diffs do
|
|
local actionType, text = diff.actionType, diff.value
|
|
local lineCount = select(2, string.gsub(text, "\n", "\n"))
|
|
local lines = string.split(text, "\n")
|
|
|
|
if actionType == StringDiff.ActionTypes.Equal then
|
|
if lineCount > 0 then
|
|
-- Jump cursor ahead to last line
|
|
currentLineNum += lineCount
|
|
incomingLineNum += lineCount
|
|
currentIdx = #lines[#lines]
|
|
incomingIdx = #lines[#lines]
|
|
else
|
|
-- Move along this line
|
|
currentIdx += #text
|
|
incomingIdx += #text
|
|
end
|
|
|
|
continue
|
|
end
|
|
|
|
if actionType == StringDiff.ActionTypes.Insert then
|
|
if firstDiffLineNum == 0 then
|
|
firstDiffLineNum = incomingLineNum
|
|
end
|
|
|
|
for i, lineText in lines do
|
|
if i > 1 then
|
|
-- Move to next line
|
|
incomingLineNum += 1
|
|
incomingIdx = 0
|
|
end
|
|
if not incomingDiffs[incomingLineNum] then
|
|
incomingDiffs[incomingLineNum] = {}
|
|
end
|
|
-- Mark these characters on this line
|
|
table.insert(incomingDiffs[incomingLineNum], {
|
|
start = incomingIdx,
|
|
stop = incomingIdx + #lineText,
|
|
})
|
|
incomingIdx += #lineText
|
|
end
|
|
elseif actionType == StringDiff.ActionTypes.Delete then
|
|
if firstDiffLineNum == 0 then
|
|
firstDiffLineNum = currentLineNum
|
|
end
|
|
|
|
for i, lineText in lines do
|
|
if i > 1 then
|
|
-- Move to next line
|
|
currentLineNum += 1
|
|
currentIdx = 0
|
|
end
|
|
if not currentDiffs[currentLineNum] then
|
|
currentDiffs[currentLineNum] = {}
|
|
end
|
|
-- Mark these characters on this line
|
|
table.insert(currentDiffs[currentLineNum], {
|
|
start = currentIdx,
|
|
stop = currentIdx + #lineText,
|
|
})
|
|
currentIdx += #lineText
|
|
end
|
|
else
|
|
Log.warn("Unknown diff action: {} {}", actionType, text)
|
|
end
|
|
end
|
|
|
|
Timer.stop()
|
|
|
|
self:setState({
|
|
maxLines = maxLines,
|
|
currentRichTextLines = currentRichTextLines,
|
|
incomingRichTextLines = incomingRichTextLines,
|
|
currentDiffs = currentDiffs,
|
|
incomingDiffs = incomingDiffs,
|
|
})
|
|
|
|
-- Scroll to the first diff line
|
|
task.defer(self.setCanvasPosition, Vector2.new(0, math.max(0, (firstDiffLineNum - 4) * 16)))
|
|
end
|
|
|
|
function StringDiffVisualizer:render()
|
|
local currentDiffs, incomingDiffs = self.state.currentDiffs, self.state.incomingDiffs
|
|
local currentRichTextLines, incomingRichTextLines =
|
|
self.state.currentRichTextLines, self.state.incomingRichTextLines
|
|
local maxLines = self.state.maxLines
|
|
|
|
return Theme.with(function(theme)
|
|
self.setLineHeight(theme.TextSize.Code)
|
|
|
|
-- Calculate the width of the canvas
|
|
-- (One line at a time to avoid the char limit of getTextBoundsAsync)
|
|
local canvasWidth = 0
|
|
for i = 1, maxLines do
|
|
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 = 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
|
|
end
|
|
end
|
|
end
|
|
|
|
local lineNumberWidth =
|
|
getTextBoundsAsync(tostring(maxLines), theme.Font.Code, theme.TextSize.Body, math.huge, true).X
|
|
|
|
canvasWidth += lineNumberWidth + 12
|
|
|
|
local removalScrollMarkers = {}
|
|
local insertionScrollMarkers = {}
|
|
for lineNum in currentDiffs 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.Background.Remove,
|
|
})
|
|
)
|
|
end
|
|
for lineNum in incomingDiffs 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.Background.Add,
|
|
})
|
|
)
|
|
end
|
|
|
|
return e(BorderedContainer, {
|
|
size = self.props.size,
|
|
position = self.props.position,
|
|
anchorPoint = self.props.anchorPoint,
|
|
transparency = self.props.transparency,
|
|
}, {
|
|
Background = e("Frame", {
|
|
Size = UDim2.new(1, 0, 1, 0),
|
|
Position = UDim2.new(0, 0, 0, 0),
|
|
BorderSizePixel = 0,
|
|
BackgroundColor3 = self.scriptBackground,
|
|
ZIndex = -10,
|
|
}, {
|
|
UICorner = e("UICorner", {
|
|
CornerRadius = UDim.new(0, 5),
|
|
}),
|
|
}),
|
|
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,
|
|
}),
|
|
Current = 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)
|
|
local lineDiffs = currentDiffs[i]
|
|
local diffFrames = table.create(if lineDiffs then #lineDiffs else 0)
|
|
|
|
-- Show diff markers over the specific changed characters
|
|
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 = if #lineDiffs == 1
|
|
and start == 0
|
|
and stop == 0
|
|
then UDim2.fromScale(1, 1)
|
|
else UDim2.new(
|
|
0,
|
|
math.max(charWidth * (stop - start), charWidth * 0.4),
|
|
1,
|
|
0
|
|
),
|
|
Position = UDim2.fromOffset(charWidth * start, 0),
|
|
BackgroundColor3 = theme.Diff.Background.Remove,
|
|
BackgroundTransparency = 0.85,
|
|
BorderSizePixel = 0,
|
|
ZIndex = -1,
|
|
})
|
|
end
|
|
end
|
|
|
|
return Roact.createFragment({
|
|
LineNumber = e("TextLabel", {
|
|
Size = UDim2.new(0, lineNumberWidth + 8, 1, 0),
|
|
Text = i,
|
|
BackgroundColor3 = Color3.new(0, 0, 0),
|
|
BackgroundTransparency = 0.9,
|
|
BorderSizePixel = 0,
|
|
FontFace = theme.Font.Code,
|
|
TextSize = theme.TextSize.Body,
|
|
TextColor3 = if lineDiffs then theme.Diff.Background.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),
|
|
BackgroundColor3 = theme.Diff.Background.Remove,
|
|
BackgroundTransparency = if lineDiffs then 0.95 else 1,
|
|
BorderSizePixel = 0,
|
|
}, {
|
|
CodeLabel = e("TextLabel", {
|
|
Size = UDim2.fromScale(1, 1),
|
|
Position = UDim2.fromScale(0, 0),
|
|
Text = currentRichTextLines[i] or "",
|
|
RichText = true,
|
|
BackgroundTransparency = 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,
|
|
}),
|
|
Incoming = 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)
|
|
local lineDiffs = incomingDiffs[i]
|
|
local diffFrames = table.create(if lineDiffs then #lineDiffs else 0)
|
|
|
|
-- Show diff markers over the specific changed characters
|
|
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 = if #lineDiffs == 1
|
|
and start == 0
|
|
and stop == 0
|
|
then UDim2.fromScale(1, 1)
|
|
else UDim2.new(
|
|
0,
|
|
math.max(charWidth * (stop - start), charWidth * 0.4),
|
|
1,
|
|
0
|
|
),
|
|
Position = UDim2.fromOffset(charWidth * start, 0),
|
|
BackgroundColor3 = theme.Diff.Background.Add,
|
|
BackgroundTransparency = 0.85,
|
|
BorderSizePixel = 0,
|
|
ZIndex = -1,
|
|
})
|
|
end
|
|
end
|
|
|
|
return Roact.createFragment({
|
|
LineNumber = e("TextLabel", {
|
|
Size = UDim2.new(0, lineNumberWidth + 8, 1, 0),
|
|
Text = i,
|
|
BackgroundColor3 = Color3.new(0, 0, 0),
|
|
BackgroundTransparency = 0.9,
|
|
BorderSizePixel = 0,
|
|
FontFace = theme.Font.Code,
|
|
TextSize = theme.TextSize.Body,
|
|
TextColor3 = if lineDiffs then theme.Diff.Background.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),
|
|
BackgroundColor3 = theme.Diff.Background.Add,
|
|
BackgroundTransparency = if lineDiffs then 0.95 else 1,
|
|
BorderSizePixel = 0,
|
|
}, {
|
|
CodeLabel = e("TextLabel", {
|
|
Size = UDim2.fromScale(1, 1),
|
|
Position = UDim2.fromScale(0, 0),
|
|
Text = incomingRichTextLines[i] or "",
|
|
RichText = true,
|
|
BackgroundColor3 = theme.Diff.Background.Add,
|
|
BackgroundTransparency = 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)
|
|
end
|
|
|
|
return StringDiffVisualizer
|