From 4018607b77985ab02b7b8439de711d62e46e7639 Mon Sep 17 00:00:00 2001 From: boatbomber Date: Mon, 22 Jan 2024 12:26:41 -0800 Subject: [PATCH] Visualize table changes (#834) Implements a pop out diff view for table properties like Attributes and Tags --- CHANGELOG.md | 6 +- .../Components/PatchVisualizer/ChangeList.lua | 248 ++++++++++-------- .../PatchVisualizer/DisplayValue.lua | 9 +- .../Components/PatchVisualizer/DomLabel.lua | 6 +- .../App/Components/PatchVisualizer/init.lua | 3 +- plugin/src/App/Components/ScrollingFrame.lua | 31 ++- .../Components/StringDiffVisualizer/init.lua | 24 +- .../Components/TableDiffVisualizer/Array.lua | 192 ++++++++++++++ .../TableDiffVisualizer/Dictionary.lua | 208 +++++++++++++++ .../Components/TableDiffVisualizer/init.lua | 48 ++++ plugin/src/App/StatusPages/Confirming.lua | 77 +++++- plugin/src/App/StatusPages/Connected.lua | 79 ++++-- 12 files changed, 755 insertions(+), 176 deletions(-) create mode 100644 plugin/src/App/Components/TableDiffVisualizer/Array.lua create mode 100644 plugin/src/App/Components/TableDiffVisualizer/Dictionary.lua create mode 100644 plugin/src/App/Components/TableDiffVisualizer/init.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 54e1cd77c..70a7d5a81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,8 @@ # Rojo Changelog ## Unreleased Changes +* Added popout diff visualizer for table properties like Attributes and Tags ([#834]) * Updated Theme to use Studio colors ([#838]) - -[#838]: https://github.com/rojo-rbx/rojo/pull/838 - * Projects may now specify rules for syncing files as if they had a different file extension. ([#813]) This is specified via a new field on project files, `syncRules`: @@ -53,6 +51,8 @@ **All** sync rules are reset between project files, so they must be specified in each one when nesting them. This is to ensure that nothing can break other projects by changing how files are synced! [#813]: https://github.com/rojo-rbx/rojo/pull/813 +[#834]: https://github.com/rojo-rbx/rojo/pull/834 +[#838]: https://github.com/rojo-rbx/rojo/pull/838 ## [7.4.0] - January 16, 2024 * Improved the visualization for array properties like Tags ([#829]) diff --git a/plugin/src/App/Components/PatchVisualizer/ChangeList.lua b/plugin/src/App/Components/PatchVisualizer/ChangeList.lua index 522dd95bb..ce01b8010 100644 --- a/plugin/src/App/Components/PatchVisualizer/ChangeList.lua +++ b/plugin/src/App/Components/PatchVisualizer/ChangeList.lua @@ -14,6 +14,123 @@ local EMPTY_TABLE = {} local e = Roact.createElement +local function ViewDiffButton(props) + return Theme.with(function(theme) + return e("TextButton", { + Text = "", + Size = UDim2.new(0.7, 0, 1, -4), + LayoutOrder = 2, + BackgroundTransparency = 1, + [Roact.Event.Activated] = props.onClick, + }, { + e(BorderedContainer, { + size = UDim2.new(1, 0, 1, 0), + transparency = 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 = 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, + }), + }), + }) + end) +end + +local function RowContent(props) + local values = props.values + local metadata = props.metadata + + if props.showStringDiff and values[1] == "Source" then + -- Special case for .Source updates + return e(ViewDiffButton, { + transparency = props.transparency, + onClick = function() + if not props.showStringDiff then + return + end + props.showStringDiff(tostring(values[2]), tostring(values[3])) + end, + }) + end + + if props.showTableDiff and (type(values[2]) == "table" or type(values[3]) == "table") then + -- Special case for table properties (like Attributes/Tags) + return e(ViewDiffButton, { + transparency = props.transparency, + onClick = function() + if not props.showTableDiff then + return + end + props.showTableDiff(values[2], values[3]) + end, + }) + end + + return Theme.with(function(theme) + return Roact.createFragment({ + ColumnB = e( + "Frame", + { + BackgroundTransparency = 1, + Size = UDim2.new(0.35, 0, 1, 0), + LayoutOrder = 2, + }, + e(DisplayValue, { + value = values[2], + transparency = props.transparency, + textColor = if metadata.isWarning + then theme.Diff.Warning + else theme.Settings.Setting.DescriptionColor, + }) + ), + ColumnC = e( + "Frame", + { + BackgroundTransparency = 1, + Size = UDim2.new(0.35, 0, 1, 0), + LayoutOrder = 3, + }, + e(DisplayValue, { + value = values[3], + transparency = props.transparency, + textColor = if metadata.isWarning + then theme.Diff.Warning + else theme.Settings.Setting.DescriptionColor, + }) + ), + }) + end) +end + local ChangeList = Roact.Component:extend("ChangeList") function ChangeList:init() @@ -36,6 +153,7 @@ function ChangeList:render() PaddingRight = UDim.new(0, 5), } + local headerRow = changes[1] local headers = e("Frame", { Size = UDim2.new(1, 0, 0, 30), BackgroundTransparency = rowTransparency, @@ -49,8 +167,8 @@ function ChangeList:render() HorizontalAlignment = Enum.HorizontalAlignment.Left, VerticalAlignment = Enum.VerticalAlignment.Center, }), - A = e("TextLabel", { - Text = tostring(changes[1][1]), + ColumnA = e("TextLabel", { + Text = tostring(headerRow[1]), BackgroundTransparency = 1, Font = Enum.Font.GothamBold, TextSize = 14, @@ -61,8 +179,8 @@ function ChangeList:render() Size = UDim2.new(0.3, 0, 1, 0), LayoutOrder = 1, }), - B = e("TextLabel", { - Text = tostring(changes[1][2]), + ColumnB = e("TextLabel", { + Text = tostring(headerRow[2]), BackgroundTransparency = 1, Font = Enum.Font.GothamBold, TextSize = 14, @@ -73,8 +191,8 @@ function ChangeList:render() Size = UDim2.new(0.35, 0, 1, 0), LayoutOrder = 2, }), - C = e("TextLabel", { - Text = tostring(changes[1][3]), + ColumnC = e("TextLabel", { + Text = tostring(headerRow[3]), BackgroundTransparency = 1, Font = Enum.Font.GothamBold, TextSize = 14, @@ -95,89 +213,6 @@ function ChangeList:render() local metadata = values[4] or EMPTY_TABLE 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", { Size = UDim2.new(1, 0, 0, 30), BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1, @@ -192,7 +227,7 @@ function ChangeList:render() HorizontalAlignment = Enum.HorizontalAlignment.Left, VerticalAlignment = Enum.VerticalAlignment.Center, }), - A = e("TextLabel", { + ColumnA = e("TextLabel", { Text = (if isWarning then "⚠ " else "") .. tostring(values[1]), BackgroundTransparency = 1, Font = Enum.Font.GothamMedium, @@ -204,32 +239,13 @@ function ChangeList:render() Size = UDim2.new(0.3, 0, 1, 0), LayoutOrder = 1, }), - B = e( - "Frame", - { - BackgroundTransparency = 1, - Size = UDim2.new(0.35, 0, 1, 0), - LayoutOrder = 2, - }, - e(DisplayValue, { - value = values[2], - transparency = props.transparency, - textColor = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor, - }) - ), - C = e( - "Frame", - { - BackgroundTransparency = 1, - Size = UDim2.new(0.35, 0, 1, 0), - LayoutOrder = 3, - }, - e(DisplayValue, { - value = values[3], - transparency = props.transparency, - textColor = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor, - }) - ), + Content = e(RowContent, { + values = values, + metadata = metadata, + transparency = props.transparency, + showStringDiff = props.showStringDiff, + showTableDiff = props.showTableDiff, + }), }) end diff --git a/plugin/src/App/Components/PatchVisualizer/DisplayValue.lua b/plugin/src/App/Components/PatchVisualizer/DisplayValue.lua index a231b879e..c6664a144 100644 --- a/plugin/src/App/Components/PatchVisualizer/DisplayValue.lua +++ b/plugin/src/App/Components/PatchVisualizer/DisplayValue.lua @@ -30,7 +30,7 @@ local function DisplayValue(props) }), }), Label = e("TextLabel", { - Text = string.format("%d,%d,%d", props.value.R * 255, props.value.G * 255, props.value.B * 255), + Text = string.format("%d, %d, %d", props.value.R * 255, props.value.G * 255, props.value.B * 255), BackgroundTransparency = 1, Font = Enum.Font.GothamMedium, TextSize = 14, @@ -104,8 +104,13 @@ local function DisplayValue(props) -- Or special text handling tostring for some? -- Will add as needed, let's see what cases arise. + local textRepresentation = string.gsub(tostring(props.value), "%s", " ") + if t == "string" then + textRepresentation = '"' .. textRepresentation .. '"' + end + return e("TextLabel", { - Text = string.gsub(tostring(props.value), "%s", " "), + Text = textRepresentation, BackgroundTransparency = 1, Font = Enum.Font.GothamMedium, TextSize = 14, diff --git a/plugin/src/App/Components/PatchVisualizer/DomLabel.lua b/plugin/src/App/Components/PatchVisualizer/DomLabel.lua index f15931f65..b2d9afe05 100644 --- a/plugin/src/App/Components/PatchVisualizer/DomLabel.lua +++ b/plugin/src/App/Components/PatchVisualizer/DomLabel.lua @@ -34,7 +34,8 @@ function Expansion:render() ChangeList = e(ChangeList, { changes = props.changeList, transparency = props.transparency, - showSourceDiff = props.showSourceDiff, + showStringDiff = props.showStringDiff, + showTableDiff = props.showTableDiff, }), }) end @@ -171,7 +172,8 @@ function DomLabel:render() indent = indent, transparency = props.transparency, changeList = props.changeList, - showSourceDiff = props.showSourceDiff, + showStringDiff = props.showStringDiff, + showTableDiff = props.showTableDiff, }) else nil, DiffIcon = if props.patchType diff --git a/plugin/src/App/Components/PatchVisualizer/init.lua b/plugin/src/App/Components/PatchVisualizer/init.lua index 59cbe09bd..67a4d7b20 100644 --- a/plugin/src/App/Components/PatchVisualizer/init.lua +++ b/plugin/src/App/Components/PatchVisualizer/init.lua @@ -76,7 +76,8 @@ function PatchVisualizer:render() changeList = node.changeList, depth = depth, transparency = self.props.transparency, - showSourceDiff = self.props.showSourceDiff, + showStringDiff = self.props.showStringDiff, + showTableDiff = self.props.showTableDiff, }) ) end diff --git a/plugin/src/App/Components/ScrollingFrame.lua b/plugin/src/App/Components/ScrollingFrame.lua index f3d11c8cc..f2113be03 100644 --- a/plugin/src/App/Components/ScrollingFrame.lua +++ b/plugin/src/App/Components/ScrollingFrame.lua @@ -10,6 +10,12 @@ local bindingUtil = require(Plugin.App.bindingUtil) local e = Roact.createElement +local scrollDirToAutoSize = { + [Enum.ScrollingDirection.X] = Enum.AutomaticSize.X, + [Enum.ScrollingDirection.Y] = Enum.AutomaticSize.Y, + [Enum.ScrollingDirection.XY] = Enum.AutomaticSize.XY, +} + local function ScrollingFrame(props) return Theme.with(function(theme) return e("ScrollingFrame", { @@ -28,16 +34,21 @@ local function ScrollingFrame(props) Size = props.size, Position = props.position, AnchorPoint = props.anchorPoint, - CanvasSize = props.contentSize:map(function(value) - return UDim2.new( - 0, - if (props.scrollingDirection and props.scrollingDirection ~= Enum.ScrollingDirection.Y) - then value.X - else 0, - 0, - value.Y - ) - end), + CanvasSize = if props.contentSize + then props.contentSize:map(function(value) + return UDim2.new( + 0, + if (props.scrollingDirection and props.scrollingDirection ~= Enum.ScrollingDirection.Y) + then value.X + else 0, + 0, + value.Y + ) + end) + else UDim2.new(), + AutomaticCanvasSize = if props.contentSize == nil + then scrollDirToAutoSize[props.scrollingDirection or Enum.ScrollingDirection.XY] + else nil, BorderSizePixel = 0, BackgroundTransparency = 1, diff --git a/plugin/src/App/Components/StringDiffVisualizer/init.lua b/plugin/src/App/Components/StringDiffVisualizer/init.lua index cbdc7dec5..60eb49a35 100644 --- a/plugin/src/App/Components/StringDiffVisualizer/init.lua +++ b/plugin/src/App/Components/StringDiffVisualizer/init.lua @@ -52,7 +52,7 @@ function StringDiffVisualizer:updateScriptBackground() end function StringDiffVisualizer:didUpdate(previousProps) - if previousProps.oldText ~= self.props.oldText or previousProps.newText ~= self.props.newText then + if previousProps.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then self:calculateContentSize() local add, remove = self:calculateDiffLines() self:setState({ @@ -63,28 +63,28 @@ function StringDiffVisualizer:didUpdate(previousProps) end function StringDiffVisualizer:calculateContentSize() - local oldText, newText = self.props.oldText, self.props.newText + local oldString, newString = self.props.oldString, self.props.newString - 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)) + local oldStringBounds = TextService:GetTextSize(oldString, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999)) + local newStringBounds = TextService:GetTextSize(newString, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999)) self.setContentSize( - Vector2.new(math.max(oldTextBounds.X, newTextBounds.X), math.max(oldTextBounds.Y, newTextBounds.Y)) + Vector2.new(math.max(oldStringBounds.X, newStringBounds.X), math.max(oldStringBounds.Y, newStringBounds.Y)) ) end function StringDiffVisualizer:calculateDiffLines() - local oldText, newText = self.props.oldText, self.props.newText + local oldString, newString = self.props.oldString, self.props.newString -- Diff the two texts local startClock = os.clock() - local diffs = StringDiff.findDiffs(oldText, newText) + local diffs = StringDiff.findDiffs(oldString, newString) local stopClock = os.clock() Log.trace( "Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections", - #oldText, - #newText, + #oldString, + #newString, math.round((stopClock - startClock) * 1000 * 1000), #diffs ) @@ -137,7 +137,7 @@ function StringDiffVisualizer:calculateDiffLines() end function StringDiffVisualizer:render() - local oldText, newText = self.props.oldText, self.props.newText + local oldString, newString = self.props.oldString, self.props.newString return Theme.with(function(theme) return e(BorderedContainer, { @@ -175,7 +175,7 @@ function StringDiffVisualizer:render() Source = e(CodeLabel, { size = UDim2.new(1, 0, 1, 0), position = UDim2.new(0, 0, 0, 0), - text = oldText, + text = oldString, lineBackground = theme.Diff.Remove, markedLines = self.state.remove, }), @@ -190,7 +190,7 @@ function StringDiffVisualizer:render() Source = e(CodeLabel, { size = UDim2.new(1, 0, 1, 0), position = UDim2.new(0, 0, 0, 0), - text = newText, + text = newString, lineBackground = theme.Diff.Add, markedLines = self.state.add, }), diff --git a/plugin/src/App/Components/TableDiffVisualizer/Array.lua b/plugin/src/App/Components/TableDiffVisualizer/Array.lua new file mode 100644 index 000000000..4fd56f3c7 --- /dev/null +++ b/plugin/src/App/Components/TableDiffVisualizer/Array.lua @@ -0,0 +1,192 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin +local Packages = Rojo.Packages + +local Roact = require(Packages.Roact) + +local Assets = require(Plugin.Assets) +local Theme = require(Plugin.App.Theme) + +local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) +local DisplayValue = require(Plugin.App.Components.PatchVisualizer.DisplayValue) + +local e = Roact.createElement + +local Array = Roact.Component:extend("Array") + +function Array:init() + self:setState({ + diff = self:calculateDiff(), + }) +end + +function Array:calculateDiff() + --[[ + Find the indexes that are added or removed from the array, + and display them side by side with gaps for the indexes that + dont exist in the opposite array. + ]] + local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {} + + local i, j = 1, 1 + local diff = {} + + while i <= #oldTable and j <= #newTable do + if oldTable[i] == newTable[j] then + table.insert(diff, { oldTable[i], newTable[j] }) -- Unchanged + i += 1 + j += 1 + elseif not table.find(newTable, oldTable[i], j) then + table.insert(diff, { oldTable[i], nil }) -- Removal + i += 1 + elseif not table.find(oldTable, newTable[j], i) then + table.insert(diff, { nil, newTable[j] }) -- Addition + j += 1 + else + if table.find(newTable, oldTable[i], j) then + table.insert(diff, { nil, newTable[j] }) -- Addition + j += 1 + else + table.insert(diff, { oldTable[i], nil }) -- Removal + i += 1 + end + end + end + + -- Handle remaining elements + while i <= #oldTable do + table.insert(diff, { oldTable[i], nil }) -- Remaining Removals + i += 1 + end + while j <= #newTable do + table.insert(diff, { nil, newTable[j] }) -- Remaining Additions + j += 1 + end + + return diff +end + +function Array:didUpdate(previousProps) + if previousProps.oldTable ~= self.props.oldTable or previousProps.newTable ~= self.props.newTable then + self:setState({ + diff = self:calculateDiff(), + }) + end +end + +function Array:render() + return Theme.with(function(theme) + local diff = self.state.diff + local lines = table.create(#diff) + + for i, element in diff do + local oldValue = element[1] + local newValue = element[2] + + local patchType = if oldValue == nil then "Add" elseif newValue == nil then "Remove" else "Remain" + + table.insert( + lines, + e("Frame", { + Size = UDim2.new(1, 0, 0, 25), + BackgroundTransparency = if patchType == "Remain" then 1 else self.props.transparency, + BackgroundColor3 = if patchType == "Remain" then theme.Diff.Row else theme.Diff[patchType], + BorderSizePixel = 0, + LayoutOrder = i, + }, { + DiffIcon = if patchType ~= "Remain" + then e("ImageLabel", { + Image = Assets.Images.Diff[patchType], + ImageColor3 = theme.AddressEntry.PlaceholderColor, + ImageTransparency = self.props.transparency, + BackgroundTransparency = 1, + Size = UDim2.new(0, 15, 0, 15), + Position = UDim2.new(0, 7, 0.5, 0), + AnchorPoint = Vector2.new(0, 0.5), + }) + else nil, + Old = e("Frame", { + Size = UDim2.new(0.5, -30, 1, 0), + Position = UDim2.new(0, 30, 0, 0), + BackgroundTransparency = 1, + }, { + Display = if oldValue ~= nil + then e(DisplayValue, { + value = oldValue, + transparency = self.props.transparency, + textColor = theme.Settings.Setting.DescriptionColor, + }) + else nil, + }), + New = e("Frame", { + Size = UDim2.new(0.5, -10, 1, 0), + Position = UDim2.new(0.5, 5, 0, 0), + BackgroundTransparency = 1, + }, { + Display = if newValue ~= nil + then e(DisplayValue, { + value = newValue, + transparency = self.props.transparency, + textColor = theme.Settings.Setting.DescriptionColor, + }) + else nil, + }), + }) + ) + end + + return Roact.createFragment({ + Headers = e("Frame", { + Size = UDim2.new(1, 0, 0, 25), + BackgroundTransparency = self.props.transparency:map(function(t) + return 0.95 + (0.05 * t) + end), + BackgroundColor3 = theme.Diff.Row, + }, { + ColumnA = e("TextLabel", { + Size = UDim2.new(0.5, -30, 1, 0), + Position = UDim2.new(0, 30, 0, 0), + BackgroundTransparency = 1, + Text = "Old", + TextXAlignment = Enum.TextXAlignment.Left, + Font = Enum.Font.GothamBold, + TextSize = 14, + TextColor3 = theme.Settings.Setting.DescriptionColor, + TextTruncate = Enum.TextTruncate.AtEnd, + }), + ColumnB = e("TextLabel", { + Size = UDim2.new(0.5, -10, 1, 0), + Position = UDim2.new(0.5, 5, 0, 0), + BackgroundTransparency = 1, + Text = "New", + TextXAlignment = Enum.TextXAlignment.Left, + Font = Enum.Font.GothamBold, + TextSize = 14, + TextColor3 = theme.Settings.Setting.DescriptionColor, + TextTruncate = Enum.TextTruncate.AtEnd, + }), + Separator = e("Frame", { + Size = UDim2.new(1, 0, 0, 1), + Position = UDim2.new(0, 0, 1, 0), + BackgroundTransparency = 0, + BorderSizePixel = 0, + BackgroundColor3 = theme.BorderedContainer.BorderColor, + }), + }), + KeyValues = e(ScrollingFrame, { + position = UDim2.new(0, 1, 0, 25), + size = UDim2.new(1, -2, 1, -27), + scrollingDirection = Enum.ScrollingDirection.Y, + transparency = self.props.transparency, + }, { + Layout = e("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Top, + }), + Lines = Roact.createFragment(lines), + }), + }) + end) +end + +return Array diff --git a/plugin/src/App/Components/TableDiffVisualizer/Dictionary.lua b/plugin/src/App/Components/TableDiffVisualizer/Dictionary.lua new file mode 100644 index 000000000..d0c32d963 --- /dev/null +++ b/plugin/src/App/Components/TableDiffVisualizer/Dictionary.lua @@ -0,0 +1,208 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin +local Packages = Rojo.Packages + +local Roact = require(Packages.Roact) + +local Assets = require(Plugin.Assets) +local Theme = require(Plugin.App.Theme) + +local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame) +local DisplayValue = require(Plugin.App.Components.PatchVisualizer.DisplayValue) + +local e = Roact.createElement + +local Dictionary = Roact.Component:extend("Dictionary") + +function Dictionary:init() + self:setState({ + diff = self:calculateDiff(), + }) +end + +function Dictionary:calculateDiff() + local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {} + + -- Diff the two tables and find the added keys, removed keys, and changed keys + local diff = {} + + for key, oldValue in oldTable do + local newValue = newTable[key] + if newValue == nil then + table.insert(diff, { + key = key, + patchType = "Remove", + }) + elseif newValue ~= oldValue then + -- Note: should this do some sort of deep comparison for various types? + table.insert(diff, { + key = key, + patchType = "Edit", + }) + else + table.insert(diff, { + key = key, + patchType = "Remain", + }) + end + end + for key in newTable do + if oldTable[key] == nil then + table.insert(diff, { + key = key, + patchType = "Add", + }) + end + end + + table.sort(diff, function(a, b) + return a.key < b.key + end) + + return diff +end + +function Dictionary:didUpdate(previousProps) + if previousProps.oldTable ~= self.props.oldTable or previousProps.newTable ~= self.props.newTable then + self:setState({ + diff = self:calculateDiff(), + }) + end +end + +function Dictionary:render() + local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {} + local diff = self.state.diff + + return Theme.with(function(theme) + local lines = table.create(#diff) + for order, line in diff do + local key = line.key + local oldValue = oldTable[key] + local newValue = newTable[key] + + table.insert( + lines, + e("Frame", { + Size = UDim2.new(1, 0, 0, 25), + LayoutOrder = order, + BorderSizePixel = 0, + BackgroundTransparency = if line.patchType == "Remain" then 1 else self.props.transparency, + BackgroundColor3 = if line.patchType == "Remain" + then theme.Diff.Row + else theme.Diff[line.patchType], + }, { + DiffIcon = if line.patchType ~= "Remain" + then e("ImageLabel", { + Image = Assets.Images.Diff[line.patchType], + ImageColor3 = theme.AddressEntry.PlaceholderColor, + ImageTransparency = self.props.transparency, + BackgroundTransparency = 1, + Size = UDim2.new(0, 15, 0, 15), + Position = UDim2.new(0, 7, 0.5, 0), + AnchorPoint = Vector2.new(0, 0.5), + }) + else nil, + KeyName = e("TextLabel", { + Size = UDim2.new(0.3, -15, 1, 0), + Position = UDim2.new(0, 30, 0, 0), + BackgroundTransparency = 1, + Text = key, + TextXAlignment = Enum.TextXAlignment.Left, + Font = Enum.Font.GothamMedium, + TextSize = 14, + TextColor3 = theme.Settings.Setting.DescriptionColor, + TextTruncate = Enum.TextTruncate.AtEnd, + }), + OldValue = e("Frame", { + Size = UDim2.new(0.35, -7, 1, 0), + Position = UDim2.new(0.3, 15, 0, 0), + BackgroundTransparency = 1, + }, { + e(DisplayValue, { + value = oldValue, + transparency = self.props.transparency, + textColor = theme.Settings.Setting.DescriptionColor, + }), + }), + NewValue = e("Frame", { + Size = UDim2.new(0.35, -8, 1, 0), + Position = UDim2.new(0.65, 8, 0, 0), + BackgroundTransparency = 1, + }, { + e(DisplayValue, { + value = newValue, + transparency = self.props.transparency, + textColor = theme.Settings.Setting.DescriptionColor, + }), + }), + }) + ) + end + + return Roact.createFragment({ + Headers = e("Frame", { + Size = UDim2.new(1, 0, 0, 25), + BackgroundTransparency = self.props.transparency:map(function(t) + return 0.95 + (0.05 * t) + end), + BackgroundColor3 = theme.Diff.Row, + }, { + ColumnA = e("TextLabel", { + Size = UDim2.new(0.3, -15, 1, 0), + Position = UDim2.new(0, 30, 0, 0), + BackgroundTransparency = 1, + Text = "Key", + TextXAlignment = Enum.TextXAlignment.Left, + Font = Enum.Font.GothamBold, + TextSize = 14, + TextColor3 = theme.Settings.Setting.DescriptionColor, + TextTruncate = Enum.TextTruncate.AtEnd, + }), + ColumnB = e("TextLabel", { + Size = UDim2.new(0.35, -7, 1, 0), + Position = UDim2.new(0.3, 15, 0, 0), + BackgroundTransparency = 1, + Text = "Old", + TextXAlignment = Enum.TextXAlignment.Left, + Font = Enum.Font.GothamBold, + TextSize = 14, + TextColor3 = theme.Settings.Setting.DescriptionColor, + TextTruncate = Enum.TextTruncate.AtEnd, + }), + ColumnC = e("TextLabel", { + Size = UDim2.new(0.35, -8, 1, 0), + Position = UDim2.new(0.65, 8, 0, 0), + BackgroundTransparency = 1, + Text = "New", + TextXAlignment = Enum.TextXAlignment.Left, + Font = Enum.Font.GothamBold, + TextSize = 14, + TextColor3 = theme.Settings.Setting.DescriptionColor, + TextTruncate = Enum.TextTruncate.AtEnd, + }), + Separator = e("Frame", { + Size = UDim2.new(1, 0, 0, 1), + Position = UDim2.new(0, 0, 1, 0), + BackgroundTransparency = 0, + BorderSizePixel = 0, + BackgroundColor3 = theme.BorderedContainer.BorderColor, + }), + }), + KeyValues = e(ScrollingFrame, { + position = UDim2.new(0, 1, 0, 25), + size = UDim2.new(1, -2, 1, -27), + scrollingDirection = Enum.ScrollingDirection.Y, + transparency = self.props.transparency, + }, { + Layout = e("UIListLayout", { + SortOrder = Enum.SortOrder.LayoutOrder, + VerticalAlignment = Enum.VerticalAlignment.Top, + }), + Lines = Roact.createFragment(lines), + }), + }) + end) +end + +return Dictionary diff --git a/plugin/src/App/Components/TableDiffVisualizer/init.lua b/plugin/src/App/Components/TableDiffVisualizer/init.lua new file mode 100644 index 000000000..2a1218a7f --- /dev/null +++ b/plugin/src/App/Components/TableDiffVisualizer/init.lua @@ -0,0 +1,48 @@ +local Rojo = script:FindFirstAncestor("Rojo") +local Plugin = Rojo.Plugin +local Packages = Rojo.Packages + +local Roact = require(Packages.Roact) + +local BorderedContainer = require(Plugin.App.Components.BorderedContainer) +local Array = require(script:FindFirstChild("Array")) +local Dictionary = require(script:FindFirstChild("Dictionary")) + +local e = Roact.createElement + +local TableDiffVisualizer = Roact.Component:extend("TableDiffVisualizer") + +function TableDiffVisualizer:render() + local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {} + + -- Ensure we're diffing tables, not mixing types + if type(oldTable) ~= "table" then + oldTable = {} + end + if type(newTable) ~= "table" then + newTable = {} + end + + local isArray = next(newTable) == 1 or next(oldTable) == 1 + + return e(BorderedContainer, { + size = self.props.size, + position = self.props.position, + anchorPoint = self.props.anchorPoint, + transparency = self.props.transparency, + }, { + Content = if isArray + then e(Array, { + oldTable = oldTable, + newTable = newTable, + transparency = self.props.transparency, + }) + else e(Dictionary, { + oldTable = oldTable, + newTable = newTable, + transparency = self.props.transparency, + }), + }) +end + +return TableDiffVisualizer diff --git a/plugin/src/App/StatusPages/Confirming.lua b/plugin/src/App/StatusPages/Confirming.lua index ca5bfba06..e5b272fda 100644 --- a/plugin/src/App/StatusPages/Confirming.lua +++ b/plugin/src/App/StatusPages/Confirming.lua @@ -14,6 +14,7 @@ local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui) local Tooltip = require(Plugin.App.Components.Tooltip) local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer) local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer) +local TableDiffVisualizer = require(Plugin.App.Components.TableDiffVisualizer) local e = Roact.createElement @@ -24,9 +25,12 @@ function ConfirmingPage:init() self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0)) self:setState({ - showingSourceDiff = false, - oldSource = "", - newSource = "", + showingStringDiff = false, + oldString = "", + newString = "", + showingTableDiff = false, + oldTable = {}, + newTable = {}, }) end @@ -63,11 +67,18 @@ function ConfirmingPage:render() patch = self.props.confirmData.patch, instanceMap = self.props.confirmData.instanceMap, - showSourceDiff = function(oldSource: string, newSource: string) + showStringDiff = function(oldString: string, newString: string) self:setState({ - showingSourceDiff = true, - oldSource = oldSource, - newSource = newSource, + showingStringDiff = true, + oldString = oldString, + newString = newString, + }) + end, + showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? }) + self:setState({ + showingTableDiff = true, + oldTable = oldTable, + newTable = newTable, }) end, }), @@ -136,10 +147,10 @@ function ConfirmingPage:render() PaddingRight = UDim.new(0, 20), }), - SourceDiff = e(StudioPluginGui, { - id = "Rojo_ConfirmingSourceDiff", - title = "Source diff", - active = self.state.showingSourceDiff, + StringDiff = e(StudioPluginGui, { + id = "Rojo_ConfirmingStringDiff", + title = "String diff", + active = self.state.showingStringDiff, isEphemeral = true, initDockState = Enum.InitialDockState.Float, @@ -151,7 +162,7 @@ function ConfirmingPage:render() onClose = function() self:setState({ - showingSourceDiff = false, + showingStringDiff = false, }) end, }, { @@ -167,8 +178,46 @@ function ConfirmingPage:render() anchorPoint = Vector2.new(0, 0), transparency = self.props.transparency, - oldText = self.state.oldSource, - newText = self.state.newSource, + oldString = self.state.oldString, + newString = self.state.newString, + }), + }), + }), + }), + + TableDiff = e(StudioPluginGui, { + id = "Rojo_ConfirmingTableDiff", + title = "Table diff", + active = self.state.showingTableDiff, + isEphemeral = true, + + initDockState = Enum.InitialDockState.Float, + overridePreviousState = true, + floatingSize = Vector2.new(500, 350), + minimumSize = Vector2.new(400, 250), + + zIndexBehavior = Enum.ZIndexBehavior.Sibling, + + onClose = function() + self:setState({ + showingTableDiff = false, + }) + end, + }, { + TooltipsProvider = e(Tooltip.Provider, nil, { + Tooltips = e(Tooltip.Container, nil), + Content = e("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + }, { + e(TableDiffVisualizer, { + size = UDim2.new(1, -10, 1, -10), + position = UDim2.new(0, 5, 0, 5), + anchorPoint = Vector2.new(0, 0), + transparency = self.props.transparency, + + oldTable = self.state.oldTable, + newTable = self.state.newTable, }), }), }), diff --git a/plugin/src/App/StatusPages/Connected.lua b/plugin/src/App/StatusPages/Connected.lua index c1aafbe69..d8055b3f3 100644 --- a/plugin/src/App/StatusPages/Connected.lua +++ b/plugin/src/App/StatusPages/Connected.lua @@ -18,6 +18,7 @@ local BorderedContainer = require(Plugin.App.Components.BorderedContainer) local Tooltip = require(Plugin.App.Components.Tooltip) local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer) local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer) +local TableDiffVisualizer = require(Plugin.App.Components.TableDiffVisualizer) local e = Roact.createElement @@ -97,7 +98,8 @@ function ChangesDrawer:render() patchTree = self.props.patchTree, - showSourceDiff = self.props.showSourceDiff, + showStringDiff = self.props.showStringDiff, + showTableDiff = self.props.showTableDiff, }), }) end) @@ -239,9 +241,9 @@ function ConnectedPage:init() self:setState({ renderChanges = false, hoveringChangeInfo = false, - showingSourceDiff = false, - oldSource = "", - newSource = "", + showingStringDiff = false, + oldString = "", + newString = "", }) self.changeInfoText, self.setChangeInfoText = Roact.createBinding("") @@ -258,7 +260,7 @@ function ConnectedPage:didUpdate(previousProps) -- New patch recieved self:startChangeInfoTextUpdater() self:setState({ - showingSourceDiff = false, + showingStringDiff = false, }) end end @@ -387,11 +389,18 @@ function ConnectedPage:render() height = self.changeDrawerHeight, layoutOrder = 5, - showSourceDiff = function(oldSource: string, newSource: string) + showStringDiff = function(oldString: string, newString: string) self:setState({ - showingSourceDiff = true, - oldSource = oldSource, - newSource = newSource, + showingStringDiff = true, + oldString = oldString, + newString = newString, + }) + end, + showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? }) + self:setState({ + showingTableDiff = true, + oldTable = oldTable, + newTable = newTable, }) end, @@ -403,10 +412,10 @@ function ConnectedPage:render() end, }), - SourceDiff = e(StudioPluginGui, { - id = "Rojo_ConnectedSourceDiff", - title = "Source diff", - active = self.state.showingSourceDiff, + StringDiff = e(StudioPluginGui, { + id = "Rojo_ConnectedStringDiff", + title = "String diff", + active = self.state.showingStringDiff, isEphemeral = true, initDockState = Enum.InitialDockState.Float, @@ -418,7 +427,7 @@ function ConnectedPage:render() onClose = function() self:setState({ - showingSourceDiff = false, + showingStringDiff = false, }) end, }, { @@ -434,8 +443,46 @@ function ConnectedPage:render() anchorPoint = Vector2.new(0, 0), transparency = self.props.transparency, - oldText = self.state.oldSource, - newText = self.state.newSource, + oldString = self.state.oldString, + newString = self.state.newString, + }), + }), + }), + }), + + TableDiff = e(StudioPluginGui, { + id = "Rojo_ConnectedTableDiff", + title = "Table diff", + active = self.state.showingTableDiff, + isEphemeral = true, + + initDockState = Enum.InitialDockState.Float, + overridePreviousState = false, + floatingSize = Vector2.new(500, 350), + minimumSize = Vector2.new(400, 250), + + zIndexBehavior = Enum.ZIndexBehavior.Sibling, + + onClose = function() + self:setState({ + showingTableDiff = false, + }) + end, + }, { + TooltipsProvider = e(Tooltip.Provider, nil, { + Tooltips = e(Tooltip.Container, nil), + Content = e("Frame", { + Size = UDim2.fromScale(1, 1), + BackgroundTransparency = 1, + }, { + e(TableDiffVisualizer, { + size = UDim2.new(1, -10, 1, -10), + position = UDim2.new(0, 5, 0, 5), + anchorPoint = Vector2.new(0, 0), + transparency = self.props.transparency, + + oldTable = self.state.oldTable, + newTable = self.state.newTable, }), }), }),