forked from rojo-rbx/rojo
View rich diffs for Source property changes (#748)
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -13,3 +13,6 @@
|
|||||||
[submodule "plugin/Packages/TestEZ"]
|
[submodule "plugin/Packages/TestEZ"]
|
||||||
path = plugin/Packages/TestEZ
|
path = plugin/Packages/TestEZ
|
||||||
url = https://github.com/roblox/testez.git
|
url = https://github.com/roblox/testez.git
|
||||||
|
[submodule "plugin/Packages/Highlighter"]
|
||||||
|
path = plugin/Packages/Highlighter
|
||||||
|
url = https://github.com/boatbomber/highlighter.git
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
* Add `plugin` flag to the `build` command that outputs to the local plugins folder ([#735])
|
* Add `plugin` flag to the `build` command that outputs to the local plugins folder ([#735])
|
||||||
* Added better support for `Font` properties ([#731])
|
* Added better support for `Font` properties ([#731])
|
||||||
* Add new plugin template to the `init` command ([#738])
|
* Add new plugin template to the `init` command ([#738])
|
||||||
|
* Added rich Source diffs in patch visualizer ([#748])
|
||||||
|
|
||||||
[#745]: https://github.com/rojo-rbx/rojo/pull/745
|
[#745]: https://github.com/rojo-rbx/rojo/pull/745
|
||||||
[#668]: https://github.com/rojo-rbx/rojo/pull/668
|
[#668]: https://github.com/rojo-rbx/rojo/pull/668
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
[#735]: https://github.com/rojo-rbx/rojo/pull/735
|
[#735]: https://github.com/rojo-rbx/rojo/pull/735
|
||||||
[#731]: https://github.com/rojo-rbx/rojo/pull/731
|
[#731]: https://github.com/rojo-rbx/rojo/pull/731
|
||||||
[#738]: https://github.com/rojo-rbx/rojo/pull/738
|
[#738]: https://github.com/rojo-rbx/rojo/pull/738
|
||||||
|
[#748]: https://github.com/rojo-rbx/rojo/pull/748
|
||||||
|
|
||||||
## [7.3.0] - April 22, 2023
|
## [7.3.0] - April 22, 2023
|
||||||
* Added `$attributes` to project format. ([#574])
|
* Added `$attributes` to project format. ([#574])
|
||||||
|
|||||||
1
plugin/Packages/Highlighter
Submodule
1
plugin/Packages/Highlighter
Submodule
Submodule plugin/Packages/Highlighter added at 09263eacfe
61
plugin/src/App/Components/CodeLabel.lua
Normal file
61
plugin/src/App/Components/CodeLabel.lua
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
local Highlighter = require(Packages.Highlighter)
|
||||||
|
Highlighter.matchStudioSettings()
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
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 e("TextLabel", {
|
||||||
|
Size = self.props.size,
|
||||||
|
Position = self.props.position,
|
||||||
|
Text = self.props.text,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.RobotoMono,
|
||||||
|
TextSize = 16,
|
||||||
|
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
|
||||||
|
|
||||||
|
return CodeLabel
|
||||||
@@ -4,8 +4,10 @@ local Packages = Rojo.Packages
|
|||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
local DisplayValue = require(script.Parent.DisplayValue)
|
local DisplayValue = require(script.Parent.DisplayValue)
|
||||||
|
|
||||||
local EMPTY_TABLE = {}
|
local EMPTY_TABLE = {}
|
||||||
@@ -93,6 +95,89 @@ function ChangeList:render()
|
|||||||
local metadata = values[4] or EMPTY_TABLE
|
local metadata = values[4] or EMPTY_TABLE
|
||||||
local isWarning = metadata.isWarning
|
local isWarning = metadata.isWarning
|
||||||
|
|
||||||
|
-- Special case for .Source updates
|
||||||
|
-- because we want to display a syntax highlighted diff for better UX
|
||||||
|
if self.props.showSourceDiff and tostring(values[1]) == "Source" then
|
||||||
|
rows[row] = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 30),
|
||||||
|
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
|
||||||
|
BackgroundColor3 = theme.Diff.Row,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
LayoutOrder = row,
|
||||||
|
}, {
|
||||||
|
Padding = e("UIPadding", pad),
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
}),
|
||||||
|
A = e("TextLabel", {
|
||||||
|
Text = (if isWarning then "⚠ " else "") .. tostring(values[1]),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
Size = UDim2.new(0.3, 0, 1, 0),
|
||||||
|
LayoutOrder = 1,
|
||||||
|
}),
|
||||||
|
Button = e("TextButton", {
|
||||||
|
Text = "",
|
||||||
|
Size = UDim2.new(0.7, 0, 1, -4),
|
||||||
|
LayoutOrder = 2,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
[Roact.Event.Activated] = function()
|
||||||
|
if props.showSourceDiff then
|
||||||
|
props.showSourceDiff(tostring(values[2]), tostring(values[3]))
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
e(BorderedContainer, {
|
||||||
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
transparency = self.props.transparency:map(function(t)
|
||||||
|
return 0.5 + (0.5 * t)
|
||||||
|
end),
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
Padding = UDim.new(0, 5),
|
||||||
|
}),
|
||||||
|
Label = e("TextLabel", {
|
||||||
|
Text = "View Diff",
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
Size = UDim2.new(0, 65, 1, 0),
|
||||||
|
LayoutOrder = 1,
|
||||||
|
}),
|
||||||
|
Icon = e("ImageLabel", {
|
||||||
|
Image = Assets.Images.Icons.Expand,
|
||||||
|
ImageColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
ImageTransparency = self.props.transparency,
|
||||||
|
|
||||||
|
Size = UDim2.new(0, 16, 0, 16),
|
||||||
|
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||||
|
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
LayoutOrder = 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
rows[row] = e("Frame", {
|
rows[row] = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 30),
|
Size = UDim2.new(1, 0, 0, 30),
|
||||||
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
|
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ function Expansion:render()
|
|||||||
ChangeList = e(ChangeList, {
|
ChangeList = e(ChangeList, {
|
||||||
changes = props.changeList,
|
changes = props.changeList,
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
|
showSourceDiff = props.showSourceDiff,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
@@ -170,6 +171,7 @@ function DomLabel:render()
|
|||||||
indent = indent,
|
indent = indent,
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
changeList = props.changeList,
|
changeList = props.changeList,
|
||||||
|
showSourceDiff = props.showSourceDiff,
|
||||||
})
|
})
|
||||||
else nil,
|
else nil,
|
||||||
DiffIcon = if props.patchType
|
DiffIcon = if props.patchType
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ function PatchVisualizer:render()
|
|||||||
changeList = node.changeList,
|
changeList = node.changeList,
|
||||||
depth = depth,
|
depth = depth,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
showSourceDiff = self.props.showSourceDiff,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,19 +23,26 @@ local function ScrollingFrame(props)
|
|||||||
BottomImage = Assets.Images.ScrollBar.Bottom,
|
BottomImage = Assets.Images.ScrollBar.Bottom,
|
||||||
|
|
||||||
ElasticBehavior = Enum.ElasticBehavior.Always,
|
ElasticBehavior = Enum.ElasticBehavior.Always,
|
||||||
ScrollingDirection = Enum.ScrollingDirection.Y,
|
ScrollingDirection = props.scrollingDirection or Enum.ScrollingDirection.Y,
|
||||||
|
|
||||||
Size = props.size,
|
Size = props.size,
|
||||||
Position = props.position,
|
Position = props.position,
|
||||||
AnchorPoint = props.anchorPoint,
|
AnchorPoint = props.anchorPoint,
|
||||||
CanvasSize = props.contentSize:map(function(value)
|
CanvasSize = props.contentSize:map(function(value)
|
||||||
return UDim2.new(0, 0, 0, value.Y)
|
return UDim2.new(
|
||||||
|
0,
|
||||||
|
if (props.scrollingDirection and props.scrollingDirection ~= Enum.ScrollingDirection.Y)
|
||||||
|
then value.X
|
||||||
|
else 0,
|
||||||
|
0,
|
||||||
|
value.Y
|
||||||
|
)
|
||||||
end),
|
end),
|
||||||
|
|
||||||
BorderSizePixel = 0,
|
BorderSizePixel = 0,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
[Roact.Change.AbsoluteSize] = props[Roact.Change.AbsoluteSize]
|
[Roact.Change.AbsoluteSize] = props[Roact.Change.AbsoluteSize],
|
||||||
}, props[Roact.Children])
|
}, props[Roact.Children])
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
441
plugin/src/App/Components/StringDiffVisualizer/StringDiff.lua
Normal file
441
plugin/src/App/Components/StringDiffVisualizer/StringDiff.lua
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
--[[
|
||||||
|
Based on DiffMatchPatch by Neil Fraser.
|
||||||
|
https://github.com/google/diff-match-patch
|
||||||
|
]]
|
||||||
|
|
||||||
|
export type DiffAction = number
|
||||||
|
export type Diff = { actionType: DiffAction, value: string }
|
||||||
|
export type Diffs = { Diff }
|
||||||
|
|
||||||
|
local StringDiff = {
|
||||||
|
ActionTypes = table.freeze({
|
||||||
|
Equal = 0,
|
||||||
|
Delete = 1,
|
||||||
|
Insert = 2,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
function StringDiff.findDiffs(text1: string, text2: string): Diffs
|
||||||
|
-- Validate inputs
|
||||||
|
if type(text1) ~= "string" or type(text2) ~= "string" then
|
||||||
|
error(
|
||||||
|
string.format(
|
||||||
|
"Invalid inputs to StringDiff.findDiffs, expected strings and got (%s, %s)",
|
||||||
|
type(text1),
|
||||||
|
type(text2)
|
||||||
|
),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Shortcut if the texts are identical
|
||||||
|
if text1 == text2 then
|
||||||
|
return { { actionType = StringDiff.ActionTypes.Equal, value = text1 } }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Trim off any shared prefix and suffix
|
||||||
|
-- These are easy to detect and can be dealt with quickly without needing a complex diff
|
||||||
|
-- and later we simply add them as Equal to the start and end of the diff
|
||||||
|
local sharedPrefix, sharedSuffix
|
||||||
|
local prefixLength = StringDiff._sharedPrefix(text1, text2)
|
||||||
|
if prefixLength > 0 then
|
||||||
|
-- Store the prefix
|
||||||
|
sharedPrefix = string.sub(text1, 1, prefixLength)
|
||||||
|
-- Now trim it off
|
||||||
|
text1 = string.sub(text1, prefixLength + 1)
|
||||||
|
text2 = string.sub(text2, prefixLength + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local suffixLength = StringDiff._sharedSuffix(text1, text2)
|
||||||
|
if suffixLength > 0 then
|
||||||
|
-- Store the suffix
|
||||||
|
sharedSuffix = string.sub(text1, -suffixLength)
|
||||||
|
-- Now trim it off
|
||||||
|
text1 = string.sub(text1, 1, -suffixLength - 1)
|
||||||
|
text2 = string.sub(text2, 1, -suffixLength - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Compute the diff on the middle block where the changes lie
|
||||||
|
local diffs = StringDiff._computeDiff(text1, text2)
|
||||||
|
|
||||||
|
-- Restore the prefix and suffix
|
||||||
|
if sharedPrefix then
|
||||||
|
table.insert(diffs, 1, { actionType = StringDiff.ActionTypes.Equal, value = sharedPrefix })
|
||||||
|
end
|
||||||
|
if sharedSuffix then
|
||||||
|
table.insert(diffs, { actionType = StringDiff.ActionTypes.Equal, value = sharedSuffix })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Cleanup the diff
|
||||||
|
diffs = StringDiff._reorderAndMerge(diffs)
|
||||||
|
|
||||||
|
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/
|
||||||
|
|
||||||
|
-- Shortcut common cases
|
||||||
|
if (#text1 == 0) or (#text2 == 0) or (string.byte(text1, 1) ~= string.byte(text2, 1)) then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local pointerMin = 1
|
||||||
|
local pointerMax = math.min(#text1, #text2)
|
||||||
|
local pointerMid = pointerMax
|
||||||
|
local pointerStart = 1
|
||||||
|
while pointerMin < pointerMid do
|
||||||
|
if string.sub(text1, pointerStart, pointerMid) == string.sub(text2, pointerStart, pointerMid) then
|
||||||
|
pointerMin = pointerMid
|
||||||
|
pointerStart = pointerMin
|
||||||
|
else
|
||||||
|
pointerMax = pointerMid
|
||||||
|
end
|
||||||
|
pointerMid = math.floor(pointerMin + (pointerMax - pointerMin) / 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return pointerMid
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._sharedSuffix(text1: string, text2: string): number
|
||||||
|
-- Uses a binary search to find the largest common suffix between the two strings
|
||||||
|
-- Performance analysis: http://neil.fraser.name/news/2007/10/09/
|
||||||
|
|
||||||
|
-- Shortcut common cases
|
||||||
|
if (#text1 == 0) or (#text2 == 0) or (string.byte(text1, -1) ~= string.byte(text2, -1)) then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local pointerMin = 1
|
||||||
|
local pointerMax = math.min(#text1, #text2)
|
||||||
|
local pointerMid = pointerMax
|
||||||
|
local pointerEnd = 1
|
||||||
|
while pointerMin < pointerMid do
|
||||||
|
if string.sub(text1, -pointerMid, -pointerEnd) == string.sub(text2, -pointerMid, -pointerEnd) then
|
||||||
|
pointerMin = pointerMid
|
||||||
|
pointerEnd = pointerMin
|
||||||
|
else
|
||||||
|
pointerMax = pointerMid
|
||||||
|
end
|
||||||
|
pointerMid = math.floor(pointerMin + (pointerMax - pointerMin) / 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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._bisect(text1: string, text2: string): Diffs
|
||||||
|
-- Find the 'middle snake' of a diff, split the problem in two
|
||||||
|
-- and return the recursively constructed diff
|
||||||
|
-- See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations
|
||||||
|
|
||||||
|
-- Cache the text lengths to prevent multiple calls
|
||||||
|
local text1Length = #text1
|
||||||
|
local text2Length = #text2
|
||||||
|
|
||||||
|
local _sub, _element
|
||||||
|
local maxD = math.ceil((text1Length + text2Length) / 2)
|
||||||
|
local vOffset = maxD
|
||||||
|
local vLength = 2 * maxD
|
||||||
|
local v1 = table.create(vLength)
|
||||||
|
local v2 = table.create(vLength)
|
||||||
|
|
||||||
|
-- Setting all elements to -1 is faster in Lua than mixing integers and nil
|
||||||
|
for x = 0, vLength - 1 do
|
||||||
|
v1[x] = -1
|
||||||
|
v2[x] = -1
|
||||||
|
end
|
||||||
|
v1[vOffset + 1] = 0
|
||||||
|
v2[vOffset + 1] = 0
|
||||||
|
local delta = text1Length - text2Length
|
||||||
|
|
||||||
|
-- If the total number of characters is odd, then
|
||||||
|
-- the front path will collide with the reverse path
|
||||||
|
local front = (delta % 2 ~= 0)
|
||||||
|
|
||||||
|
-- Offsets for start and end of k loop
|
||||||
|
-- Prevents mapping of space beyond the grid
|
||||||
|
local k1Start = 0
|
||||||
|
local k1End = 0
|
||||||
|
local k2Start = 0
|
||||||
|
local k2End = 0
|
||||||
|
for d = 0, maxD - 1 do
|
||||||
|
-- Walk the front path one step
|
||||||
|
for k1 = -d + k1Start, d - k1End, 2 do
|
||||||
|
local k1_offset = vOffset + k1
|
||||||
|
local x1
|
||||||
|
if (k1 == -d) or ((k1 ~= d) and (v1[k1_offset - 1] < v1[k1_offset + 1])) then
|
||||||
|
x1 = v1[k1_offset + 1]
|
||||||
|
else
|
||||||
|
x1 = v1[k1_offset - 1] + 1
|
||||||
|
end
|
||||||
|
local y1 = x1 - k1
|
||||||
|
while
|
||||||
|
(x1 <= text1Length)
|
||||||
|
and (y1 <= text2Length)
|
||||||
|
and (string.sub(text1, x1, x1) == string.sub(text2, y1, y1))
|
||||||
|
do
|
||||||
|
x1 = x1 + 1
|
||||||
|
y1 = y1 + 1
|
||||||
|
end
|
||||||
|
v1[k1_offset] = x1
|
||||||
|
if x1 > text1Length + 1 then
|
||||||
|
-- Ran off the right of the graph
|
||||||
|
k1End = k1End + 2
|
||||||
|
elseif y1 > text2Length + 1 then
|
||||||
|
-- Ran off the bottom of the graph
|
||||||
|
k1Start = k1Start + 2
|
||||||
|
elseif front then
|
||||||
|
local k2_offset = vOffset + delta - k1
|
||||||
|
if k2_offset >= 0 and k2_offset < vLength and v2[k2_offset] ~= -1 then
|
||||||
|
-- Mirror x2 onto top-left coordinate system
|
||||||
|
local x2 = text1Length - v2[k2_offset] + 1
|
||||||
|
if x1 > x2 then
|
||||||
|
-- Overlap detected
|
||||||
|
return StringDiff._bisectSplit(text1, text2, x1, y1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Walk the reverse path one step
|
||||||
|
for k2 = -d + k2Start, d - k2End, 2 do
|
||||||
|
local k2_offset = vOffset + k2
|
||||||
|
local x2
|
||||||
|
if (k2 == -d) or ((k2 ~= d) and (v2[k2_offset - 1] < v2[k2_offset + 1])) then
|
||||||
|
x2 = v2[k2_offset + 1]
|
||||||
|
else
|
||||||
|
x2 = v2[k2_offset - 1] + 1
|
||||||
|
end
|
||||||
|
local y2 = x2 - k2
|
||||||
|
while
|
||||||
|
(x2 <= text1Length)
|
||||||
|
and (y2 <= text2Length)
|
||||||
|
and (string.sub(text1, -x2, -x2) == string.sub(text2, -y2, -y2))
|
||||||
|
do
|
||||||
|
x2 = x2 + 1
|
||||||
|
y2 = y2 + 1
|
||||||
|
end
|
||||||
|
v2[k2_offset] = x2
|
||||||
|
if x2 > text1Length + 1 then
|
||||||
|
-- Ran off the left of the graph
|
||||||
|
k2End = k2End + 2
|
||||||
|
elseif y2 > text2Length + 1 then
|
||||||
|
-- Ran off the top of the graph
|
||||||
|
k2Start = k2Start + 2
|
||||||
|
elseif not front then
|
||||||
|
local k1_offset = vOffset + delta - k2
|
||||||
|
if k1_offset >= 0 and k1_offset < vLength and v1[k1_offset] ~= -1 then
|
||||||
|
local x1 = v1[k1_offset]
|
||||||
|
local y1 = vOffset + x1 - k1_offset
|
||||||
|
-- Mirror x2 onto top-left coordinate system
|
||||||
|
x2 = text1Length - x2 + 1
|
||||||
|
if x1 > x2 then
|
||||||
|
-- Overlap detected
|
||||||
|
return StringDiff._bisectSplit(text1, text2, x1, y1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Number of diffs equals number of characters, no commonality at all
|
||||||
|
return {
|
||||||
|
{ actionType = StringDiff.ActionTypes.Delete, value = text1 },
|
||||||
|
{ actionType = StringDiff.ActionTypes.Insert, value = text2 },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._bisectSplit(text1: string, text2: string, x: number, y: number): Diffs
|
||||||
|
-- Given the location of the 'middle snake',
|
||||||
|
-- split the diff in two parts and recurse
|
||||||
|
|
||||||
|
local text1a = string.sub(text1, 1, x - 1)
|
||||||
|
local text2a = string.sub(text2, 1, y - 1)
|
||||||
|
local text1b = string.sub(text1, x)
|
||||||
|
local text2b = string.sub(text2, y)
|
||||||
|
|
||||||
|
-- Compute both diffs serially
|
||||||
|
local diffs = StringDiff.findDiffs(text1a, text2a)
|
||||||
|
local diffsB = StringDiff.findDiffs(text1b, text2b)
|
||||||
|
|
||||||
|
-- Merge diffs
|
||||||
|
table.move(diffsB, 1, #diffsB, #diffs + 1, diffs)
|
||||||
|
return diffs
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._reorderAndMerge(diffs: Diffs): Diffs
|
||||||
|
-- Reorder and merge like edit sections and merge equalities
|
||||||
|
-- Any edit section can move as long as it doesn't cross an equality
|
||||||
|
|
||||||
|
-- Add a dummy entry at the end
|
||||||
|
table.insert(diffs, { actionType = StringDiff.ActionTypes.Equal, value = "" })
|
||||||
|
|
||||||
|
local pointer = 1
|
||||||
|
local countDelete, countInsert = 0, 0
|
||||||
|
local textDelete, textInsert = "", ""
|
||||||
|
local commonLength
|
||||||
|
while diffs[pointer] do
|
||||||
|
local actionType = diffs[pointer].actionType
|
||||||
|
if actionType == StringDiff.ActionTypes.Insert then
|
||||||
|
countInsert = countInsert + 1
|
||||||
|
textInsert = textInsert .. diffs[pointer].value
|
||||||
|
pointer = pointer + 1
|
||||||
|
elseif actionType == StringDiff.ActionTypes.Delete then
|
||||||
|
countDelete = countDelete + 1
|
||||||
|
textDelete = textDelete .. diffs[pointer].value
|
||||||
|
pointer = pointer + 1
|
||||||
|
elseif actionType == StringDiff.ActionTypes.Equal then
|
||||||
|
-- Upon reaching an equality, check for prior redundancies
|
||||||
|
if countDelete + countInsert > 1 then
|
||||||
|
if (countDelete > 0) and (countInsert > 0) then
|
||||||
|
-- Factor out any common prefixies
|
||||||
|
commonLength = StringDiff._sharedPrefix(textInsert, textDelete)
|
||||||
|
if commonLength > 0 then
|
||||||
|
local back_pointer = pointer - countDelete - countInsert
|
||||||
|
if
|
||||||
|
(back_pointer > 1) and (diffs[back_pointer - 1].actionType == StringDiff.ActionTypes.Equal)
|
||||||
|
then
|
||||||
|
diffs[back_pointer - 1].value = diffs[back_pointer - 1].value
|
||||||
|
.. string.sub(textInsert, 1, commonLength)
|
||||||
|
else
|
||||||
|
table.insert(diffs, 1, {
|
||||||
|
actionType = StringDiff.ActionTypes.Equal,
|
||||||
|
value = string.sub(textInsert, 1, commonLength),
|
||||||
|
})
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
textInsert = string.sub(textInsert, commonLength + 1)
|
||||||
|
textDelete = string.sub(textDelete, commonLength + 1)
|
||||||
|
end
|
||||||
|
-- Factor out any common suffixies
|
||||||
|
commonLength = StringDiff._sharedSuffix(textInsert, textDelete)
|
||||||
|
if commonLength ~= 0 then
|
||||||
|
diffs[pointer].value = string.sub(textInsert, -commonLength) .. diffs[pointer].value
|
||||||
|
textInsert = string.sub(textInsert, 1, -commonLength - 1)
|
||||||
|
textDelete = string.sub(textDelete, 1, -commonLength - 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Delete the offending records and add the merged ones
|
||||||
|
pointer = pointer - countDelete - countInsert
|
||||||
|
for _ = 1, countDelete + countInsert do
|
||||||
|
table.remove(diffs, pointer)
|
||||||
|
end
|
||||||
|
if #textDelete > 0 then
|
||||||
|
table.insert(diffs, pointer, { actionType = StringDiff.ActionTypes.Delete, value = textDelete })
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
if #textInsert > 0 then
|
||||||
|
table.insert(diffs, pointer, { actionType = StringDiff.ActionTypes.Insert, value = textInsert })
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
pointer = pointer + 1
|
||||||
|
elseif (pointer > 1) and (diffs[pointer - 1].actionType == StringDiff.ActionTypes.Equal) then
|
||||||
|
-- Merge this equality with the previous one
|
||||||
|
diffs[pointer - 1].value = diffs[pointer - 1].value .. diffs[pointer].value
|
||||||
|
table.remove(diffs, pointer)
|
||||||
|
else
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
countInsert, countDelete = 0, 0
|
||||||
|
textDelete, textInsert = "", ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if diffs[#diffs].value == "" then
|
||||||
|
-- Remove the dummy entry at the end
|
||||||
|
diffs[#diffs] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Second pass: look for single edits surrounded on both sides by equalities
|
||||||
|
-- which can be shifted sideways to eliminate an equality
|
||||||
|
-- e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
|
||||||
|
local changes = false
|
||||||
|
pointer = 2
|
||||||
|
-- Intentionally ignore the first and last element (don't need checking)
|
||||||
|
while pointer < #diffs 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 currentDiff = diffs[pointer]
|
||||||
|
local currentText = currentDiff.value
|
||||||
|
local prevText = prevDiff.value
|
||||||
|
local nextText = nextDiff.value
|
||||||
|
if #prevText == 0 then
|
||||||
|
table.remove(diffs, pointer - 1)
|
||||||
|
changes = true
|
||||||
|
elseif string.sub(currentText, -#prevText) == prevText then
|
||||||
|
-- Shift the edit over the previous equality
|
||||||
|
currentDiff.value = prevText .. string.sub(currentText, 1, -#prevText - 1)
|
||||||
|
nextDiff.value = prevText .. nextDiff.value
|
||||||
|
table.remove(diffs, pointer - 1)
|
||||||
|
changes = true
|
||||||
|
elseif string.sub(currentText, 1, #nextText) == nextText then
|
||||||
|
-- Shift the edit over the next equality
|
||||||
|
prevDiff.value = prevText .. nextText
|
||||||
|
currentDiff.value = string.sub(currentText, #nextText + 1) .. nextText
|
||||||
|
table.remove(diffs, pointer + 1)
|
||||||
|
changes = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If shifts were made, the diffs need reordering and another shift sweep
|
||||||
|
if changes then
|
||||||
|
return StringDiff._reorderAndMerge(diffs)
|
||||||
|
end
|
||||||
|
|
||||||
|
return diffs
|
||||||
|
end
|
||||||
|
|
||||||
|
return StringDiff
|
||||||
202
plugin/src/App/Components/StringDiffVisualizer/init.lua
Normal file
202
plugin/src/App/Components/StringDiffVisualizer/init.lua
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
local TextService = game:GetService("TextService")
|
||||||
|
|
||||||
|
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)
|
||||||
|
local StringDiff = require(script:FindFirstChild("StringDiff"))
|
||||||
|
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
|
local CodeLabel = require(Plugin.App.Components.CodeLabel)
|
||||||
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
|
|
||||||
|
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.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
self:updateScriptBackground()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
self:calculateContentSize()
|
||||||
|
self:updateScriptBackground()
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
add = {},
|
||||||
|
remove = {},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:willUnmount()
|
||||||
|
self.themeChangedConnection:Disconnect()
|
||||||
|
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.oldText ~= self.props.oldText or previousProps.newText ~= self.props.newText then
|
||||||
|
self:calculateContentSize()
|
||||||
|
local add, remove = self:calculateDiffLines()
|
||||||
|
self:setState({
|
||||||
|
add = add,
|
||||||
|
remove = remove,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:calculateContentSize()
|
||||||
|
local oldText, newText = self.props.oldText, self.props.newText
|
||||||
|
|
||||||
|
local oldTextBounds = TextService:GetTextSize(oldText, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
||||||
|
local newTextBounds = TextService:GetTextSize(newText, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
||||||
|
|
||||||
|
self.setContentSize(
|
||||||
|
Vector2.new(math.max(oldTextBounds.X, newTextBounds.X), math.max(oldTextBounds.Y, newTextBounds.Y))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:calculateDiffLines()
|
||||||
|
local oldText, newText = self.props.oldText, self.props.newText
|
||||||
|
|
||||||
|
-- Diff the two texts
|
||||||
|
local startClock = os.clock()
|
||||||
|
local diffs = StringDiff.findDiffs(oldText, newText)
|
||||||
|
local stopClock = os.clock()
|
||||||
|
|
||||||
|
Log.trace(
|
||||||
|
"Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections",
|
||||||
|
#oldText,
|
||||||
|
#newText,
|
||||||
|
math.round((stopClock - startClock) * 1000 * 1000),
|
||||||
|
#diffs
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Determine which lines to highlight
|
||||||
|
local add, remove = {}, {}
|
||||||
|
|
||||||
|
local oldLineNum, newLineNum = 1, 1
|
||||||
|
for _, diff in diffs do
|
||||||
|
local actionType, text = diff.actionType, diff.value
|
||||||
|
local lines = select(2, string.gsub(text, "\n", "\n"))
|
||||||
|
|
||||||
|
if actionType == StringDiff.ActionTypes.Equal then
|
||||||
|
oldLineNum += lines
|
||||||
|
newLineNum += lines
|
||||||
|
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
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if string.match(text, "%S") then
|
||||||
|
add[newLineNum] = true
|
||||||
|
end
|
||||||
|
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
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if string.match(text, "%S") then
|
||||||
|
remove[oldLineNum] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
oldLineNum += lines
|
||||||
|
else
|
||||||
|
Log.warn("Unknown diff action: {} {}", actionType, text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return add, remove
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:render()
|
||||||
|
local oldText, newText = self.props.oldText, self.props.newText
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
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(ScrollingFrame, {
|
||||||
|
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 = oldText,
|
||||||
|
lineBackground = theme.Diff.Remove,
|
||||||
|
markedLines = self.state.remove,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
New = e(ScrollingFrame, {
|
||||||
|
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 = newText,
|
||||||
|
lineBackground = theme.Diff.Add,
|
||||||
|
markedLines = self.state.add,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return StringDiffVisualizer
|
||||||
@@ -76,6 +76,12 @@ function StudioPluginGui:didUpdate(lastProps)
|
|||||||
if self.props.active ~= lastProps.active then
|
if self.props.active ~= lastProps.active then
|
||||||
-- This is intentionally in didUpdate to make sure the initial active state
|
-- This is intentionally in didUpdate to make sure the initial active state
|
||||||
-- (if the PluginGui is open initially) is preserved.
|
-- (if the PluginGui is open initially) is preserved.
|
||||||
|
|
||||||
|
-- Studio widgets are very unreliable and sometimes need to be flickered
|
||||||
|
-- in order to force them to render correctly
|
||||||
|
-- This happens within a single frame so it doesn't flicker visibly
|
||||||
|
self.pluginGui.Enabled = self.props.active
|
||||||
|
self.pluginGui.Enabled = not self.props.active
|
||||||
self.pluginGui.Enabled = self.props.active
|
self.pluginGui.Enabled = self.props.active
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ local Header = require(Plugin.App.Components.Header)
|
|||||||
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
|
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
|
||||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||||
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
||||||
|
local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -21,6 +22,12 @@ local ConfirmingPage = Roact.Component:extend("ConfirmingPage")
|
|||||||
function ConfirmingPage:init()
|
function ConfirmingPage:init()
|
||||||
self.contentSize, self.setContentSize = Roact.createBinding(0)
|
self.contentSize, self.setContentSize = Roact.createBinding(0)
|
||||||
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
showingSourceDiff = false,
|
||||||
|
oldSource = "",
|
||||||
|
newSource = "",
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
function ConfirmingPage:render()
|
function ConfirmingPage:render()
|
||||||
@@ -55,6 +62,14 @@ function ConfirmingPage:render()
|
|||||||
changeListHeaders = { "Property", "Current", "Incoming" },
|
changeListHeaders = { "Property", "Current", "Incoming" },
|
||||||
patch = self.props.confirmData.patch,
|
patch = self.props.confirmData.patch,
|
||||||
instanceMap = self.props.confirmData.instanceMap,
|
instanceMap = self.props.confirmData.instanceMap,
|
||||||
|
|
||||||
|
showSourceDiff = function(oldSource: string, newSource: string)
|
||||||
|
self:setState({
|
||||||
|
showingSourceDiff = true,
|
||||||
|
oldSource = oldSource,
|
||||||
|
newSource = newSource,
|
||||||
|
})
|
||||||
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Buttons = e("Frame", {
|
Buttons = e("Frame", {
|
||||||
@@ -120,6 +135,43 @@ function ConfirmingPage:render()
|
|||||||
PaddingLeft = UDim.new(0, 20),
|
PaddingLeft = UDim.new(0, 20),
|
||||||
PaddingRight = UDim.new(0, 20),
|
PaddingRight = UDim.new(0, 20),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
SourceDiff = e(StudioPluginGui, {
|
||||||
|
id = "Rojo_ConfirmingSourceDiff",
|
||||||
|
title = "Source diff",
|
||||||
|
active = self.state.showingSourceDiff,
|
||||||
|
|
||||||
|
initDockState = Enum.InitialDockState.Float,
|
||||||
|
overridePreviousState = true,
|
||||||
|
floatingSize = Vector2.new(500, 350),
|
||||||
|
minimumSize = Vector2.new(400, 250),
|
||||||
|
|
||||||
|
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
|
||||||
|
onClose = function()
|
||||||
|
self:setState({
|
||||||
|
showingSourceDiff = false,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
TooltipsProvider = e(Tooltip.Provider, nil, {
|
||||||
|
Tooltips = e(Tooltip.Container, nil),
|
||||||
|
Content = e("Frame", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
e(StringDiffVisualizer, {
|
||||||
|
size = UDim2.new(1, -10, 1, -10),
|
||||||
|
position = UDim2.new(0, 5, 0, 5),
|
||||||
|
anchorPoint = Vector2.new(0, 0),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
|
oldText = self.state.oldSource,
|
||||||
|
newText = self.state.newSource,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if self.props.createPopup then
|
if self.props.createPopup then
|
||||||
@@ -132,7 +184,6 @@ function ConfirmingPage:render()
|
|||||||
active = true,
|
active = true,
|
||||||
|
|
||||||
initDockState = Enum.InitialDockState.Float,
|
initDockState = Enum.InitialDockState.Float,
|
||||||
initEnabled = true,
|
|
||||||
overridePreviousState = true,
|
overridePreviousState = true,
|
||||||
floatingSize = Vector2.new(500, 350),
|
floatingSize = Vector2.new(500, 350),
|
||||||
minimumSize = Vector2.new(400, 250),
|
minimumSize = Vector2.new(400, 250),
|
||||||
|
|||||||
@@ -10,12 +10,14 @@ local Theme = require(Plugin.App.Theme)
|
|||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local PatchSet = require(Plugin.PatchSet)
|
local PatchSet = require(Plugin.PatchSet)
|
||||||
|
|
||||||
|
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
|
||||||
local Header = require(Plugin.App.Components.Header)
|
local Header = require(Plugin.App.Components.Header)
|
||||||
local IconButton = require(Plugin.App.Components.IconButton)
|
local IconButton = require(Plugin.App.Components.IconButton)
|
||||||
local TextButton = require(Plugin.App.Components.TextButton)
|
local TextButton = require(Plugin.App.Components.TextButton)
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||||
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
||||||
|
local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ function timeSinceText(elapsed: number): string
|
|||||||
return ageText
|
return ageText
|
||||||
end
|
end
|
||||||
|
|
||||||
local ChangesDrawer = Roact.Component:extend("ConnectedPage")
|
local ChangesDrawer = Roact.Component:extend("ChangesDrawer")
|
||||||
|
|
||||||
function ChangesDrawer:init()
|
function ChangesDrawer:init()
|
||||||
-- Hold onto the serve session during the lifecycle of this component
|
-- Hold onto the serve session during the lifecycle of this component
|
||||||
@@ -84,6 +86,8 @@ function ChangesDrawer:render()
|
|||||||
layoutOrder = 3,
|
layoutOrder = 3,
|
||||||
|
|
||||||
patchTree = self.props.patchTree,
|
patchTree = self.props.patchTree,
|
||||||
|
|
||||||
|
showSourceDiff = self.props.showSourceDiff,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
@@ -226,6 +230,9 @@ function ConnectedPage:init()
|
|||||||
self:setState({
|
self:setState({
|
||||||
renderChanges = false,
|
renderChanges = false,
|
||||||
hoveringChangeInfo = false,
|
hoveringChangeInfo = false,
|
||||||
|
showingSourceDiff = false,
|
||||||
|
oldSource = "",
|
||||||
|
newSource = "",
|
||||||
})
|
})
|
||||||
|
|
||||||
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
|
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
|
||||||
@@ -239,7 +246,11 @@ end
|
|||||||
|
|
||||||
function ConnectedPage:didUpdate(previousProps)
|
function ConnectedPage:didUpdate(previousProps)
|
||||||
if self.props.patchData.timestamp ~= previousProps.patchData.timestamp then
|
if self.props.patchData.timestamp ~= previousProps.patchData.timestamp then
|
||||||
|
-- New patch recieved
|
||||||
self:startChangeInfoTextUpdater()
|
self:startChangeInfoTextUpdater()
|
||||||
|
self:setState({
|
||||||
|
showingSourceDiff = false,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -367,6 +378,14 @@ function ConnectedPage:render()
|
|||||||
height = self.changeDrawerHeight,
|
height = self.changeDrawerHeight,
|
||||||
layoutOrder = 5,
|
layoutOrder = 5,
|
||||||
|
|
||||||
|
showSourceDiff = function(oldSource: string, newSource: string)
|
||||||
|
self:setState({
|
||||||
|
showingSourceDiff = true,
|
||||||
|
oldSource = oldSource,
|
||||||
|
newSource = newSource,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
|
||||||
onClose = function()
|
onClose = function()
|
||||||
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
||||||
frequency = 4,
|
frequency = 4,
|
||||||
@@ -374,6 +393,43 @@ function ConnectedPage:render()
|
|||||||
}))
|
}))
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
SourceDiff = e(StudioPluginGui, {
|
||||||
|
id = "Rojo_ConnectedSourceDiff",
|
||||||
|
title = "Source diff",
|
||||||
|
active = self.state.showingSourceDiff,
|
||||||
|
|
||||||
|
initDockState = Enum.InitialDockState.Float,
|
||||||
|
overridePreviousState = true,
|
||||||
|
floatingSize = Vector2.new(500, 350),
|
||||||
|
minimumSize = Vector2.new(400, 250),
|
||||||
|
|
||||||
|
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
|
||||||
|
onClose = function()
|
||||||
|
self:setState({
|
||||||
|
showingSourceDiff = false,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
TooltipsProvider = e(Tooltip.Provider, nil, {
|
||||||
|
Tooltips = e(Tooltip.Container, nil),
|
||||||
|
Content = e("Frame", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
e(StringDiffVisualizer, {
|
||||||
|
size = UDim2.new(1, -10, 1, -10),
|
||||||
|
position = UDim2.new(0, 5, 0, 5),
|
||||||
|
anchorPoint = Vector2.new(0, 0),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
|
oldText = self.state.oldSource,
|
||||||
|
newText = self.state.newSource,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -554,7 +554,6 @@ function App:render()
|
|||||||
active = self.state.guiEnabled,
|
active = self.state.guiEnabled,
|
||||||
|
|
||||||
initDockState = Enum.InitialDockState.Right,
|
initDockState = Enum.InitialDockState.Right,
|
||||||
initEnabled = false,
|
|
||||||
overridePreviousState = false,
|
overridePreviousState = false,
|
||||||
floatingSize = Vector2.new(320, 210),
|
floatingSize = Vector2.new(320, 210),
|
||||||
minimumSize = Vector2.new(300, 210),
|
minimumSize = Vector2.new(300, 210),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ local Assets = {
|
|||||||
Close = "rbxassetid://6012985953",
|
Close = "rbxassetid://6012985953",
|
||||||
Back = "rbxassetid://6017213752",
|
Back = "rbxassetid://6017213752",
|
||||||
Reset = "rbxassetid://10142422327",
|
Reset = "rbxassetid://10142422327",
|
||||||
|
Expand = "rbxassetid://12045401097",
|
||||||
},
|
},
|
||||||
Diff = {
|
Diff = {
|
||||||
Add = "rbxassetid://10434145835",
|
Add = "rbxassetid://10434145835",
|
||||||
|
|||||||
Reference in New Issue
Block a user