diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83af35e6..732a2bf2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,59 @@
## Unreleased Changes
* Added headless API for Studio companion plugins. ([#631])
+* Added popout diff visualizer for table properties like Attributes and Tags ([#834])
+* Updated Theme to use Studio colors ([#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`:
+
+ ```json
+ {
+ "syncRules": [
+ {
+ "pattern": "*.foo",
+ "use": "text",
+ "exclude": "*.exclude.foo",
+ },
+ {
+ "pattern": "*.bar.baz",
+ "use": "json",
+ "suffix": ".bar.baz",
+ },
+ ],
+ "name": "SyncRulesAreCool",
+ "tree": {
+ "$path": "src"
+ }
+ }
+ ```
+
+ The `pattern` field is a glob used to match the sync rule to files. If present, the `suffix` field allows you to specify parts of a file's name get cut off by Rojo to name the Instance, including the file extension. If it isn't specified, Rojo will only cut off the first part of the file extension, up to the first dot.
+
+ Additionally, the `exclude` field allows files to be excluded from the sync rule if they match a pattern specified by it. If it's not present, all files that match `pattern` will be modified using the sync rule.
+
+ The `use` field corresponds to one of the potential file type that Rojo will currently include in a project. Files that match the provided pattern will be treated as if they had the file extension for that file type. A full list is below:
+
+ | `use` value | file extension |
+ |:---------------|:----------------|
+ | `serverScript` | `.server.lua` |
+ | `clientScript` | `.client.lua` |
+ | `moduleScript` | `.lua` |
+ | `json` | `.json` |
+ | `toml` | `.toml` |
+ | `csv` | `.csv` |
+ | `text` | `.txt` |
+ | `jsonModel` | `.model.json` |
+ | `rbxm` | `.rbxm` |
+ | `rbxmx` | `.rbxmx` |
+ | `project` | `.project.json` |
+ | `ignore` | None! |
+
+ **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!
[#631]: https://github.com/rojo-rbx/rojo/pull/631
+[#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/Dropdown.lua b/plugin/src/App/Components/Dropdown.lua
index 4b761636..3fde88c5 100644
--- a/plugin/src/App/Components/Dropdown.lua
+++ b/plugin/src/App/Components/Dropdown.lua
@@ -109,9 +109,7 @@ function Dropdown:render()
}, {
DropArrow = e("ImageLabel", {
Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow,
- ImageColor3 = self.openBinding:map(function(a)
- return theme.Closed.IconColor:Lerp(theme.Open.IconColor, a)
- end),
+ ImageColor3 = theme.IconColor,
ImageTransparency = self.props.transparency,
Size = UDim2.new(0, 18, 0, 18),
diff --git a/plugin/src/App/Components/PatchVisualizer/ChangeList.lua b/plugin/src/App/Components/PatchVisualizer/ChangeList.lua
index 7aac862a..ce01b801 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,36 +167,36 @@ 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,
- TextColor3 = theme.Settings.Setting.DescriptionColor,
+ TextColor3 = theme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
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,
- TextColor3 = theme.Settings.Setting.DescriptionColor,
+ TextColor3 = theme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
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,
- TextColor3 = theme.Settings.Setting.DescriptionColor,
+ TextColor3 = theme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
@@ -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,44 +227,25 @@ 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,
TextSize = 14,
- TextColor3 = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
+ TextColor3 = if isWarning then theme.Diff.Warning else theme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
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 a231b879..c6664a14 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 8495d712..b2d9afe0 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
@@ -205,7 +207,7 @@ function DomLabel:render()
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
- TextColor3 = if props.isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
+ TextColor3 = if props.isWarning then theme.Diff.Warning else theme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
diff --git a/plugin/src/App/Components/PatchVisualizer/init.lua b/plugin/src/App/Components/PatchVisualizer/init.lua
index aad2e63d..67a4d7b2 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
@@ -98,7 +99,7 @@ function PatchVisualizer:render()
Text = "No changes to sync, project is up to date.",
Font = Enum.Font.GothamMedium,
TextSize = 15,
- TextColor3 = theme.Settings.Setting.DescriptionColor,
+ TextColor3 = theme.TextColor,
TextWrapped = true,
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1,
diff --git a/plugin/src/App/Components/ScrollingFrame.lua b/plugin/src/App/Components/ScrollingFrame.lua
index 05a10283..12b0189e 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", {
@@ -29,16 +35,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 cbdc7dec..60eb49a3 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 00000000..4fd56f3c
--- /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 00000000..d0c32d96
--- /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 00000000..2a1218a7
--- /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 457340bd..e5b272fd 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
@@ -47,7 +51,7 @@ function ConfirmingPage:render()
Font = Enum.Font.Gotham,
LineHeight = 1.2,
TextSize = 14,
- TextColor3 = theme.Settings.Setting.DescriptionColor,
+ TextColor3 = theme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 0, 20),
@@ -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 c1aafbe6..d8055b3f 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,
}),
}),
}),
diff --git a/plugin/src/App/Theme.lua b/plugin/src/App/Theme.lua
index d6f9622b..2ba8fc9d 100644
--- a/plugin/src/App/Theme.lua
+++ b/plugin/src/App/Theme.lua
@@ -25,227 +25,7 @@ local strict = require(script.Parent.Parent.strict)
local BRAND_COLOR = Color3.fromHex("E13835")
-local lightTheme = strict("LightTheme", {
- BackgroundColor = Color3.fromHex("FFFFFF"),
- Button = {
- Solid = {
- ActionFillColor = Color3.fromHex("FFFFFF"),
- ActionFillTransparency = 0.8,
- Enabled = {
- TextColor = Color3.fromHex("FFFFFF"),
- BackgroundColor = BRAND_COLOR,
- },
- Disabled = {
- TextColor = Color3.fromHex("FFFFFF"),
- BackgroundColor = BRAND_COLOR,
- },
- },
- Bordered = {
- ActionFillColor = Color3.fromHex("000000"),
- ActionFillTransparency = 0.9,
- Enabled = {
- TextColor = Color3.fromHex("393939"),
- BorderColor = Color3.fromHex("ACACAC"),
- },
- Disabled = {
- TextColor = Color3.fromHex("393939"),
- BorderColor = Color3.fromHex("ACACAC"),
- },
- },
- },
- Checkbox = {
- Active = {
- IconColor = Color3.fromHex("FFFFFF"),
- BackgroundColor = BRAND_COLOR,
- },
- Inactive = {
- IconColor = Color3.fromHex("EEEEEE"),
- BorderColor = Color3.fromHex("AFAFAF"),
- },
- },
- Dropdown = {
- TextColor = Color3.fromHex("000000"),
- BorderColor = Color3.fromHex("AFAFAF"),
- BackgroundColor = Color3.fromHex("EEEEEE"),
- Open = {
- IconColor = BRAND_COLOR,
- },
- Closed = {
- IconColor = Color3.fromHex("EEEEEE"),
- },
- },
- TextInput = {
- Enabled = {
- TextColor = Color3.fromHex("000000"),
- PlaceholderColor = Color3.fromHex("8C8C8C"),
- BorderColor = Color3.fromHex("ACACAC"),
- },
- Disabled = {
- TextColor = Color3.fromHex("393939"),
- PlaceholderColor = Color3.fromHex("8C8C8C"),
- BorderColor = Color3.fromHex("AFAFAF"),
- },
- ActionFillColor = Color3.fromHex("000000"),
- ActionFillTransparency = 0.9,
- },
- AddressEntry = {
- TextColor = Color3.fromHex("000000"),
- PlaceholderColor = Color3.fromHex("8C8C8C"),
- },
- BorderedContainer = {
- BorderColor = Color3.fromHex("CBCBCB"),
- BackgroundColor = Color3.fromHex("EEEEEE"),
- },
- Spinner = {
- ForegroundColor = BRAND_COLOR,
- BackgroundColor = Color3.fromHex("EEEEEE"),
- },
- Diff = {
- Add = Color3.fromHex("baffbd"),
- Remove = Color3.fromHex("ffbdba"),
- Edit = Color3.fromHex("bacdff"),
- Row = Color3.fromHex("000000"),
- Warning = Color3.fromHex("FF8E3C"),
- },
- ConnectionDetails = {
- ProjectNameColor = Color3.fromHex("000000"),
- AddressColor = Color3.fromHex("000000"),
- DisconnectColor = BRAND_COLOR,
- },
- Settings = {
- DividerColor = Color3.fromHex("CBCBCB"),
- Navbar = {
- BackButtonColor = Color3.fromHex("000000"),
- TextColor = Color3.fromHex("000000"),
- },
- Setting = {
- NameColor = Color3.fromHex("000000"),
- DescriptionColor = Color3.fromHex("5F5F5F"),
- },
- },
- Header = {
- LogoColor = BRAND_COLOR,
- VersionColor = Color3.fromHex("727272"),
- },
- Notification = {
- InfoColor = Color3.fromHex("000000"),
- CloseColor = BRAND_COLOR,
- },
- ErrorColor = Color3.fromHex("000000"),
- ScrollBarColor = Color3.fromHex("000000"),
-})
-
-local darkTheme = strict("DarkTheme", {
- BackgroundColor = Color3.fromHex("2E2E2E"),
- Button = {
- Solid = {
- ActionFillColor = Color3.fromHex("FFFFFF"),
- ActionFillTransparency = 0.8,
- Enabled = {
- TextColor = Color3.fromHex("FFFFFF"),
- BackgroundColor = BRAND_COLOR,
- },
- Disabled = {
- TextColor = Color3.fromHex("FFFFFF"),
- BackgroundColor = BRAND_COLOR,
- },
- },
- Bordered = {
- ActionFillColor = Color3.fromHex("FFFFFF"),
- ActionFillTransparency = 0.9,
- Enabled = {
- TextColor = Color3.fromHex("DBDBDB"),
- BorderColor = Color3.fromHex("535353"),
- },
- Disabled = {
- TextColor = Color3.fromHex("DBDBDB"),
- BorderColor = Color3.fromHex("535353"),
- },
- },
- },
- Checkbox = {
- Active = {
- IconColor = Color3.fromHex("FFFFFF"),
- BackgroundColor = BRAND_COLOR,
- },
- Inactive = {
- IconColor = Color3.fromHex("484848"),
- BorderColor = Color3.fromHex("5A5A5A"),
- },
- },
- Dropdown = {
- TextColor = Color3.fromHex("FFFFFF"),
- BorderColor = Color3.fromHex("5A5A5A"),
- BackgroundColor = Color3.fromHex("2B2B2B"),
- Open = {
- IconColor = BRAND_COLOR,
- },
- Closed = {
- IconColor = Color3.fromHex("484848"),
- },
- },
- TextInput = {
- Enabled = {
- TextColor = Color3.fromHex("FFFFFF"),
- PlaceholderColor = Color3.fromHex("8B8B8B"),
- BorderColor = Color3.fromHex("535353"),
- },
- Disabled = {
- TextColor = Color3.fromHex("484848"),
- PlaceholderColor = Color3.fromHex("8B8B8B"),
- BorderColor = Color3.fromHex("5A5A5A"),
- },
- ActionFillColor = Color3.fromHex("FFFFFF"),
- ActionFillTransparency = 0.9,
- },
- AddressEntry = {
- TextColor = Color3.fromHex("FFFFFF"),
- PlaceholderColor = Color3.fromHex("8B8B8B"),
- },
- BorderedContainer = {
- BorderColor = Color3.fromHex("535353"),
- BackgroundColor = Color3.fromHex("2B2B2B"),
- },
- Spinner = {
- ForegroundColor = BRAND_COLOR,
- BackgroundColor = Color3.fromHex("2B2B2B"),
- },
- Diff = {
- Add = Color3.fromHex("273732"),
- Remove = Color3.fromHex("3F2D32"),
- Edit = Color3.fromHex("193345"),
- Row = Color3.fromHex("FFFFFF"),
- Warning = Color3.fromHex("FF8E3C"),
- },
- ConnectionDetails = {
- ProjectNameColor = Color3.fromHex("FFFFFF"),
- AddressColor = Color3.fromHex("FFFFFF"),
- DisconnectColor = Color3.fromHex("FFFFFF"),
- },
- Settings = {
- DividerColor = Color3.fromHex("535353"),
- Navbar = {
- BackButtonColor = Color3.fromHex("FFFFFF"),
- TextColor = Color3.fromHex("FFFFFF"),
- },
- Setting = {
- NameColor = Color3.fromHex("FFFFFF"),
- DescriptionColor = Color3.fromHex("D3D3D3"),
- },
- },
- Header = {
- LogoColor = BRAND_COLOR,
- VersionColor = Color3.fromHex("D3D3D3"),
- },
- Notification = {
- InfoColor = Color3.fromHex("FFFFFF"),
- CloseColor = Color3.fromHex("FFFFFF"),
- },
- ErrorColor = Color3.fromHex("FFFFFF"),
- ScrollBarColor = Color3.fromHex("FFFFFF"),
-})
-
-local Context = Roact.createContext(lightTheme)
+local Context = Roact.createContext({})
local StudioProvider = Roact.Component:extend("StudioProvider")
@@ -253,21 +33,153 @@ local StudioProvider = Roact.Component:extend("StudioProvider")
function StudioProvider:updateTheme()
local studioTheme = getStudio().Theme
- if studioTheme.Name == "Light" then
- self:setState({
- theme = lightTheme,
- })
- elseif studioTheme.Name == "Dark" then
- self:setState({
- theme = darkTheme,
- })
- else
- Log.warn("Unexpected theme '{}'' -- falling back to light theme!", studioTheme.Name)
+ local theme = strict(studioTheme.Name .. "Theme", {
+ BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
+ TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
+ Button = {
+ Solid = {
+ -- Solid uses brand theming, not Studio theming.
+ ActionFillColor = Color3.fromHex("FFFFFF"),
+ ActionFillTransparency = 0.8,
+ Enabled = {
+ TextColor = Color3.fromHex("FFFFFF"),
+ BackgroundColor = BRAND_COLOR,
+ },
+ Disabled = {
+ TextColor = Color3.fromHex("FFFFFF"),
+ BackgroundColor = BRAND_COLOR,
+ },
+ },
+ Bordered = {
+ ActionFillColor = studioTheme:GetColor(
+ Enum.StudioStyleGuideColor.ButtonText,
+ Enum.StudioStyleGuideModifier.Selected
+ ),
+ ActionFillTransparency = 0.9,
+ Enabled = {
+ TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.ButtonText),
+ BorderColor = studioTheme:GetColor(
+ Enum.StudioStyleGuideColor.CheckedFieldBorder,
+ Enum.StudioStyleGuideModifier.Disabled
+ ),
+ },
+ Disabled = {
+ TextColor = studioTheme:GetColor(
+ Enum.StudioStyleGuideColor.ButtonText,
+ Enum.StudioStyleGuideModifier.Disabled
+ ),
+ BorderColor = studioTheme:GetColor(
+ Enum.StudioStyleGuideColor.CheckedFieldBorder,
+ Enum.StudioStyleGuideModifier.Disabled
+ ),
+ },
+ },
+ },
+ Checkbox = {
+ Active = {
+ -- Active checkboxes use brand theming, not Studio theming.
+ IconColor = Color3.fromHex("FFFFFF"),
+ BackgroundColor = BRAND_COLOR,
+ },
+ Inactive = {
+ IconColor = studioTheme:GetColor(
+ Enum.StudioStyleGuideColor.CheckedFieldIndicator,
+ Enum.StudioStyleGuideModifier.Disabled
+ ),
+ BorderColor = studioTheme:GetColor(
+ Enum.StudioStyleGuideColor.CheckedFieldBorder,
+ Enum.StudioStyleGuideModifier.Disabled
+ ),
+ },
+ },
+ Dropdown = {
+ TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.ButtonText),
+ BorderColor = studioTheme:GetColor(
+ Enum.StudioStyleGuideColor.CheckedFieldBorder,
+ Enum.StudioStyleGuideModifier.Disabled
+ ),
+ BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainBackground),
+ IconColor = studioTheme:GetColor(
+ Enum.StudioStyleGuideColor.CheckedFieldIndicator,
+ Enum.StudioStyleGuideModifier.Disabled
+ ),
+ },
+ TextInput = {
+ Enabled = {
+ TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ PlaceholderColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText),
+ BorderColor = studioTheme:GetColor(
+ Enum.StudioStyleGuideColor.CheckedFieldBorder,
+ Enum.StudioStyleGuideModifier.Disabled
+ ),
+ },
+ Disabled = {
+ TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
+ PlaceholderColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText),
+ BorderColor = studioTheme:GetColor(
+ Enum.StudioStyleGuideColor.CheckedFieldBorder,
+ Enum.StudioStyleGuideModifier.Disabled
+ ),
+ },
+ ActionFillColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ ActionFillTransparency = 0.9,
+ },
+ AddressEntry = {
+ TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ PlaceholderColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.SubText),
+ },
+ BorderedContainer = {
+ BorderColor = studioTheme:GetColor(
+ Enum.StudioStyleGuideColor.CheckedFieldBorder,
+ Enum.StudioStyleGuideModifier.Disabled
+ ),
+ BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
+ },
+ Spinner = {
+ ForegroundColor = BRAND_COLOR,
+ BackgroundColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.InputFieldBackground),
+ },
+ Diff = {
+ Add = studioTheme:GetColor(Enum.StudioStyleGuideColor.DiffTextAdditionBackground),
+ Remove = studioTheme:GetColor(Enum.StudioStyleGuideColor.DiffTextDeletionBackground),
+ Edit = studioTheme:GetColor(Enum.StudioStyleGuideColor.DiffLineNumSeparatorBackground),
+ Row = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ Warning = studioTheme:GetColor(Enum.StudioStyleGuideColor.WarningText),
+ },
+ ConnectionDetails = {
+ ProjectNameColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ AddressColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ DisconnectColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ },
+ Settings = {
+ DividerColor = studioTheme:GetColor(
+ Enum.StudioStyleGuideColor.CheckedFieldBorder,
+ Enum.StudioStyleGuideModifier.Disabled
+ ),
+ Navbar = {
+ BackButtonColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ TextColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ },
+ Setting = {
+ NameColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ DescriptionColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
+ },
+ },
+ Header = {
+ LogoColor = BRAND_COLOR,
+ VersionColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.MainText),
+ },
+ Notification = {
+ InfoColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ CloseColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ },
+ ErrorColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ ScrollBarColor = studioTheme:GetColor(Enum.StudioStyleGuideColor.BrightText),
+ })
- self:setState({
- theme = lightTheme,
- })
- end
+ self:setState({
+ theme = theme,
+ })
end
function StudioProvider:init()
diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_alone.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_alone.snap
new file mode 100644
index 00000000..53e329cc
--- /dev/null
+++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_alone.snap
@@ -0,0 +1,18 @@
+---
+source: tests/tests/build.rs
+assertion_line: 102
+expression: contents
+---
+
+ -
+
+ sync_rule_alone
+
+
-
+
+ foo
+ Hello, world!
+
+
+
+
diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_complex.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_complex.snap
new file mode 100644
index 00000000..e38050d8
--- /dev/null
+++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_complex.snap
@@ -0,0 +1,43 @@
+---
+source: tests/tests/build.rs
+assertion_line: 104
+expression: contents
+---
+
+ -
+
+ sync_rule_complex
+
+
-
+
+ bar
+ 0
+ -- Hello, from bar (a Script)!
+
+
+ -
+
+ baz
+ -- Hello, from baz (a LocalScript)!
+
+
+ -
+
+ cat
+ Hello, from cat (a StringValue)!
+
+
+ -
+
+ foo
+ -- Hello, from foo (a ModuleScript)!
+
+
+ -
+
+ qux
+ Hello, from qux (a .rojo file that's turned into a StringValue)!
+
+
+
+
diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_nested_projects.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_nested_projects.snap
new file mode 100644
index 00000000..6867ac37
--- /dev/null
+++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__sync_rule_nested_projects.snap
@@ -0,0 +1,12 @@
+---
+source: tests/tests/build.rs
+assertion_line: 104
+expression: contents
+---
+
+ -
+
+ sync_rule_nested_projects
+
+
+
diff --git a/rojo-test/build-tests/sync_rule_alone/default.project.json b/rojo-test/build-tests/sync_rule_alone/default.project.json
new file mode 100644
index 00000000..f3a5aa34
--- /dev/null
+++ b/rojo-test/build-tests/sync_rule_alone/default.project.json
@@ -0,0 +1,12 @@
+{
+ "name": "sync_rule_alone",
+ "tree": {
+ "$path": "src"
+ },
+ "syncRules": [
+ {
+ "pattern": "*.nothing",
+ "use": "text"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/rojo-test/build-tests/sync_rule_alone/src/foo.nothing b/rojo-test/build-tests/sync_rule_alone/src/foo.nothing
new file mode 100644
index 00000000..5dd01c17
--- /dev/null
+++ b/rojo-test/build-tests/sync_rule_alone/src/foo.nothing
@@ -0,0 +1 @@
+Hello, world!
\ No newline at end of file
diff --git a/rojo-test/build-tests/sync_rule_complex/default.project.json b/rojo-test/build-tests/sync_rule_complex/default.project.json
new file mode 100644
index 00000000..62764bb4
--- /dev/null
+++ b/rojo-test/build-tests/sync_rule_complex/default.project.json
@@ -0,0 +1,30 @@
+{
+ "name": "sync_rule_complex",
+ "tree": {
+ "$path": "src"
+ },
+ "syncRules": [
+ {
+ "pattern": "*.module",
+ "use": "moduleScript"
+ },
+ {
+ "pattern": "*.server",
+ "use": "serverScript"
+ },
+ {
+ "pattern": "*.client",
+ "use": "clientScript"
+ },
+ {
+ "pattern": "*.rojo",
+ "exclude": "*.ignore.rojo",
+ "use": "project"
+ },
+ {
+ "pattern": "*.dog.rojo2",
+ "use": "text",
+ "suffix": ".dog.rojo2"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/rojo-test/build-tests/sync_rule_complex/src/bar.server b/rojo-test/build-tests/sync_rule_complex/src/bar.server
new file mode 100644
index 00000000..e860bd77
--- /dev/null
+++ b/rojo-test/build-tests/sync_rule_complex/src/bar.server
@@ -0,0 +1 @@
+-- Hello, from bar (a Script)!
\ No newline at end of file
diff --git a/rojo-test/build-tests/sync_rule_complex/src/baz.client b/rojo-test/build-tests/sync_rule_complex/src/baz.client
new file mode 100644
index 00000000..4326a2a4
--- /dev/null
+++ b/rojo-test/build-tests/sync_rule_complex/src/baz.client
@@ -0,0 +1 @@
+-- Hello, from baz (a LocalScript)!
\ No newline at end of file
diff --git a/rojo-test/build-tests/sync_rule_complex/src/cat.dog.rojo2 b/rojo-test/build-tests/sync_rule_complex/src/cat.dog.rojo2
new file mode 100644
index 00000000..e185da86
--- /dev/null
+++ b/rojo-test/build-tests/sync_rule_complex/src/cat.dog.rojo2
@@ -0,0 +1 @@
+Hello, from cat (a StringValue)!
\ No newline at end of file
diff --git a/rojo-test/build-tests/sync_rule_complex/src/foo.module b/rojo-test/build-tests/sync_rule_complex/src/foo.module
new file mode 100644
index 00000000..3a55f9b2
--- /dev/null
+++ b/rojo-test/build-tests/sync_rule_complex/src/foo.module
@@ -0,0 +1 @@
+-- Hello, from foo (a ModuleScript)!
\ No newline at end of file
diff --git a/rojo-test/build-tests/sync_rule_complex/src/qux.rojo b/rojo-test/build-tests/sync_rule_complex/src/qux.rojo
new file mode 100644
index 00000000..7153477d
--- /dev/null
+++ b/rojo-test/build-tests/sync_rule_complex/src/qux.rojo
@@ -0,0 +1,9 @@
+{
+ "name": "qux",
+ "tree": {
+ "$className": "StringValue",
+ "$properties": {
+ "Value": "Hello, from qux (a .rojo file that's turned into a StringValue)!"
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/build-tests/sync_rule_complex/src/rat.ignore.rojo b/rojo-test/build-tests/sync_rule_complex/src/rat.ignore.rojo
new file mode 100644
index 00000000..d80a8cc8
--- /dev/null
+++ b/rojo-test/build-tests/sync_rule_complex/src/rat.ignore.rojo
@@ -0,0 +1 @@
+This file should be ignored!
\ No newline at end of file
diff --git a/rojo-test/build-tests/sync_rule_nested_projects/default.project.json b/rojo-test/build-tests/sync_rule_nested_projects/default.project.json
new file mode 100644
index 00000000..1b6193bb
--- /dev/null
+++ b/rojo-test/build-tests/sync_rule_nested_projects/default.project.json
@@ -0,0 +1,12 @@
+{
+ "name": "sync_rule_nested_projects",
+ "tree": {
+ "$path": "nested.project.json"
+ },
+ "syncRules": [
+ {
+ "pattern": "*.rojo",
+ "use": "text"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/rojo-test/build-tests/sync_rule_nested_projects/nested.project.json b/rojo-test/build-tests/sync_rule_nested_projects/nested.project.json
new file mode 100644
index 00000000..3c5fea2c
--- /dev/null
+++ b/rojo-test/build-tests/sync_rule_nested_projects/nested.project.json
@@ -0,0 +1,12 @@
+{
+ "name": "nested",
+ "tree": {
+ "$path": "src"
+ },
+ "syncRules": [
+ {
+ "pattern": "*.txt",
+ "use": "ignore"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/rojo-test/build-tests/sync_rule_nested_projects/src/ignored.rojo b/rojo-test/build-tests/sync_rule_nested_projects/src/ignored.rojo
new file mode 100644
index 00000000..d7ed8b4f
--- /dev/null
+++ b/rojo-test/build-tests/sync_rule_nested_projects/src/ignored.rojo
@@ -0,0 +1 @@
+This shouldn't be in the built file. If it is, something is wrong.
\ No newline at end of file
diff --git a/rojo-test/build-tests/sync_rule_nested_projects/src/ignored.txt b/rojo-test/build-tests/sync_rule_nested_projects/src/ignored.txt
new file mode 100644
index 00000000..d7ed8b4f
--- /dev/null
+++ b/rojo-test/build-tests/sync_rule_nested_projects/src/ignored.txt
@@ -0,0 +1 @@
+This shouldn't be in the built file. If it is, something is wrong.
\ No newline at end of file
diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_all.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_all.snap
new file mode 100644
index 00000000..e4fa4adf
--- /dev/null
+++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_all.snap
@@ -0,0 +1,30 @@
+---
+source: tests/tests/serve.rs
+assertion_line: 268
+expression: "read_response.intern_and_redact(&mut redactions, root_id)"
+---
+instances:
+ id-2:
+ Children:
+ - id-3
+ ClassName: Folder
+ Id: id-2
+ Metadata:
+ ignoreUnknownInstances: false
+ Name: sync_rule_alone
+ Parent: "00000000000000000000000000000000"
+ Properties: {}
+ id-3:
+ Children: []
+ ClassName: StringValue
+ Id: id-3
+ Metadata:
+ ignoreUnknownInstances: false
+ Name: foo
+ Parent: id-2
+ Properties:
+ Value:
+ String: "Hello, world!"
+messageCursor: 0
+sessionId: id-1
+
diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_info.snap
new file mode 100644
index 00000000..514b8858
--- /dev/null
+++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_alone_info.snap
@@ -0,0 +1,14 @@
+---
+source: tests/tests/serve.rs
+assertion_line: 265
+expression: redactions.redacted_yaml(info)
+---
+expectedPlaceIds: ~
+gameId: ~
+placeId: ~
+projectName: sync_rule_alone
+protocolVersion: 4
+rootInstanceId: id-2
+serverVersion: "[server-version]"
+sessionId: id-1
+
diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_all.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_all.snap
new file mode 100644
index 00000000..80cd76f6
--- /dev/null
+++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_all.snap
@@ -0,0 +1,80 @@
+---
+source: tests/tests/serve.rs
+assertion_line: 284
+expression: "read_response.intern_and_redact(&mut redactions, root_id)"
+---
+instances:
+ id-2:
+ Children:
+ - id-3
+ - id-4
+ - id-5
+ - id-6
+ - id-7
+ ClassName: Folder
+ Id: id-2
+ Metadata:
+ ignoreUnknownInstances: false
+ Name: sync_rule_complex
+ Parent: "00000000000000000000000000000000"
+ Properties: {}
+ id-3:
+ Children: []
+ ClassName: Script
+ Id: id-3
+ Metadata:
+ ignoreUnknownInstances: false
+ Name: bar
+ Parent: id-2
+ Properties:
+ RunContext:
+ Enum: 0
+ Source:
+ String: "-- Hello, from bar (a Script)!"
+ id-4:
+ Children: []
+ ClassName: LocalScript
+ Id: id-4
+ Metadata:
+ ignoreUnknownInstances: false
+ Name: baz
+ Parent: id-2
+ Properties:
+ Source:
+ String: "-- Hello, from baz (a LocalScript)!"
+ id-5:
+ Children: []
+ ClassName: StringValue
+ Id: id-5
+ Metadata:
+ ignoreUnknownInstances: false
+ Name: cat
+ Parent: id-2
+ Properties:
+ Value:
+ String: "Hello, from cat (a StringValue)!"
+ id-6:
+ Children: []
+ ClassName: ModuleScript
+ Id: id-6
+ Metadata:
+ ignoreUnknownInstances: false
+ Name: foo
+ Parent: id-2
+ Properties:
+ Source:
+ String: "-- Hello, from foo (a ModuleScript)!"
+ id-7:
+ Children: []
+ ClassName: StringValue
+ Id: id-7
+ Metadata:
+ ignoreUnknownInstances: true
+ Name: qux
+ Parent: id-2
+ Properties:
+ Value:
+ String: "Hello, from qux (a .rojo file that's turned into a StringValue)!"
+messageCursor: 0
+sessionId: id-1
+
diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_info.snap
new file mode 100644
index 00000000..3ed2b805
--- /dev/null
+++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_complex_info.snap
@@ -0,0 +1,14 @@
+---
+source: tests/tests/serve.rs
+assertion_line: 281
+expression: redactions.redacted_yaml(info)
+---
+expectedPlaceIds: ~
+gameId: ~
+placeId: ~
+projectName: sync_rule_complex
+protocolVersion: 4
+rootInstanceId: id-2
+serverVersion: "[server-version]"
+sessionId: id-1
+
diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_all.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_all.snap
new file mode 100644
index 00000000..f3210d3c
--- /dev/null
+++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_all.snap
@@ -0,0 +1,30 @@
+---
+source: tests/tests/serve.rs
+assertion_line: 303
+expression: "read_response.intern_and_redact(&mut redactions, root_id)"
+---
+instances:
+ id-2:
+ Children:
+ - id-3
+ ClassName: Folder
+ Id: id-2
+ Metadata:
+ ignoreUnknownInstances: false
+ Name: sync_rule_no_extension
+ Parent: "00000000000000000000000000000000"
+ Properties: {}
+ id-3:
+ Children: []
+ ClassName: ModuleScript
+ Id: id-3
+ Metadata:
+ ignoreUnknownInstances: false
+ Name: no_extension
+ Parent: id-2
+ Properties:
+ Source:
+ String: "return {\"This file has no extension but should be a ModuleScript named `no_extension`\"}"
+messageCursor: 0
+sessionId: id-1
+
diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_info.snap
new file mode 100644
index 00000000..3503e595
--- /dev/null
+++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__sync_rule_no_extension_info.snap
@@ -0,0 +1,14 @@
+---
+source: tests/tests/serve.rs
+assertion_line: 297
+expression: redactions.redacted_yaml(info)
+---
+expectedPlaceIds: ~
+gameId: ~
+placeId: ~
+projectName: sync_rule_no_extension
+protocolVersion: 4
+rootInstanceId: id-2
+serverVersion: "[server-version]"
+sessionId: id-1
+
diff --git a/rojo-test/serve-tests/sync_rule_alone/default.project.json b/rojo-test/serve-tests/sync_rule_alone/default.project.json
new file mode 100644
index 00000000..f3a5aa34
--- /dev/null
+++ b/rojo-test/serve-tests/sync_rule_alone/default.project.json
@@ -0,0 +1,12 @@
+{
+ "name": "sync_rule_alone",
+ "tree": {
+ "$path": "src"
+ },
+ "syncRules": [
+ {
+ "pattern": "*.nothing",
+ "use": "text"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/rojo-test/serve-tests/sync_rule_alone/src/foo.nothing b/rojo-test/serve-tests/sync_rule_alone/src/foo.nothing
new file mode 100644
index 00000000..5dd01c17
--- /dev/null
+++ b/rojo-test/serve-tests/sync_rule_alone/src/foo.nothing
@@ -0,0 +1 @@
+Hello, world!
\ No newline at end of file
diff --git a/rojo-test/serve-tests/sync_rule_complex/default.project.json b/rojo-test/serve-tests/sync_rule_complex/default.project.json
new file mode 100644
index 00000000..62764bb4
--- /dev/null
+++ b/rojo-test/serve-tests/sync_rule_complex/default.project.json
@@ -0,0 +1,30 @@
+{
+ "name": "sync_rule_complex",
+ "tree": {
+ "$path": "src"
+ },
+ "syncRules": [
+ {
+ "pattern": "*.module",
+ "use": "moduleScript"
+ },
+ {
+ "pattern": "*.server",
+ "use": "serverScript"
+ },
+ {
+ "pattern": "*.client",
+ "use": "clientScript"
+ },
+ {
+ "pattern": "*.rojo",
+ "exclude": "*.ignore.rojo",
+ "use": "project"
+ },
+ {
+ "pattern": "*.dog.rojo2",
+ "use": "text",
+ "suffix": ".dog.rojo2"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/rojo-test/serve-tests/sync_rule_complex/src/bar.server b/rojo-test/serve-tests/sync_rule_complex/src/bar.server
new file mode 100644
index 00000000..e860bd77
--- /dev/null
+++ b/rojo-test/serve-tests/sync_rule_complex/src/bar.server
@@ -0,0 +1 @@
+-- Hello, from bar (a Script)!
\ No newline at end of file
diff --git a/rojo-test/serve-tests/sync_rule_complex/src/baz.client b/rojo-test/serve-tests/sync_rule_complex/src/baz.client
new file mode 100644
index 00000000..4326a2a4
--- /dev/null
+++ b/rojo-test/serve-tests/sync_rule_complex/src/baz.client
@@ -0,0 +1 @@
+-- Hello, from baz (a LocalScript)!
\ No newline at end of file
diff --git a/rojo-test/serve-tests/sync_rule_complex/src/cat.dog.rojo2 b/rojo-test/serve-tests/sync_rule_complex/src/cat.dog.rojo2
new file mode 100644
index 00000000..e185da86
--- /dev/null
+++ b/rojo-test/serve-tests/sync_rule_complex/src/cat.dog.rojo2
@@ -0,0 +1 @@
+Hello, from cat (a StringValue)!
\ No newline at end of file
diff --git a/rojo-test/serve-tests/sync_rule_complex/src/foo.module b/rojo-test/serve-tests/sync_rule_complex/src/foo.module
new file mode 100644
index 00000000..3a55f9b2
--- /dev/null
+++ b/rojo-test/serve-tests/sync_rule_complex/src/foo.module
@@ -0,0 +1 @@
+-- Hello, from foo (a ModuleScript)!
\ No newline at end of file
diff --git a/rojo-test/serve-tests/sync_rule_complex/src/qux.rojo b/rojo-test/serve-tests/sync_rule_complex/src/qux.rojo
new file mode 100644
index 00000000..7153477d
--- /dev/null
+++ b/rojo-test/serve-tests/sync_rule_complex/src/qux.rojo
@@ -0,0 +1,9 @@
+{
+ "name": "qux",
+ "tree": {
+ "$className": "StringValue",
+ "$properties": {
+ "Value": "Hello, from qux (a .rojo file that's turned into a StringValue)!"
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/serve-tests/sync_rule_complex/src/rat.ignore.rojo b/rojo-test/serve-tests/sync_rule_complex/src/rat.ignore.rojo
new file mode 100644
index 00000000..d80a8cc8
--- /dev/null
+++ b/rojo-test/serve-tests/sync_rule_complex/src/rat.ignore.rojo
@@ -0,0 +1 @@
+This file should be ignored!
\ No newline at end of file
diff --git a/rojo-test/serve-tests/sync_rule_no_extension/default.project.json b/rojo-test/serve-tests/sync_rule_no_extension/default.project.json
new file mode 100644
index 00000000..185c6633
--- /dev/null
+++ b/rojo-test/serve-tests/sync_rule_no_extension/default.project.json
@@ -0,0 +1,12 @@
+{
+ "name": "sync_rule_no_extension",
+ "tree": {
+ "$path": "src"
+ },
+ "syncRules": [
+ {
+ "pattern": "src/**",
+ "use": "json"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/rojo-test/serve-tests/sync_rule_no_extension/src/no_extension b/rojo-test/serve-tests/sync_rule_no_extension/src/no_extension
new file mode 100644
index 00000000..41c705c4
--- /dev/null
+++ b/rojo-test/serve-tests/sync_rule_no_extension/src/no_extension
@@ -0,0 +1 @@
+["This file has no extension but should be a ModuleScript named `no_extension`"]
\ No newline at end of file
diff --git a/src/project.rs b/src/project.rs
index 93a3cb21..1095fba2 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -8,7 +8,7 @@ use std::{
use serde::{Deserialize, Serialize};
use thiserror::Error;
-use crate::{glob::Glob, resolution::UnresolvedValue};
+use crate::{glob::Glob, resolution::UnresolvedValue, snapshot::SyncRule};
static PROJECT_FILENAME: &str = "default.project.json";
@@ -84,6 +84,12 @@ pub struct Project {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub glob_ignore_paths: Vec,
+ /// A list of mappings of globs to syncing rules. If a file matches a glob,
+ /// it will be 'transformed' into an Instance following the rule provided.
+ /// Globs are relative to the folder the project file is in.
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub sync_rules: Vec,
+
/// The path to the file that this project came from. Relative paths in the
/// project should be considered relative to the parent of this field, also
/// given by `Project::folder_location`.
diff --git a/src/snapshot/metadata.rs b/src/snapshot/metadata.rs
index 1a3273ed..1578af23 100644
--- a/src/snapshot/metadata.rs
+++ b/src/snapshot/metadata.rs
@@ -4,11 +4,14 @@ use std::{
sync::Arc,
};
+use anyhow::Context;
use serde::{Deserialize, Serialize};
use crate::{
- glob::Glob, path_serializer, project::ProjectNode,
- snapshot_middleware::emit_legacy_scripts_default,
+ glob::Glob,
+ path_serializer,
+ project::ProjectNode,
+ snapshot_middleware::{emit_legacy_scripts_default, Middleware},
};
/// Rojo-specific metadata that can be associated with an instance or a snapshot
@@ -107,6 +110,8 @@ pub struct InstanceContext {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub path_ignore_rules: Arc>,
pub emit_legacy_scripts: bool,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ pub sync_rules: Vec,
}
impl InstanceContext {
@@ -114,6 +119,7 @@ impl InstanceContext {
Self {
path_ignore_rules: Arc::new(Vec::new()),
emit_legacy_scripts: emit_legacy_scripts_default().unwrap(),
+ sync_rules: Vec::new(),
}
}
@@ -144,9 +150,28 @@ impl InstanceContext {
rules.extend(new_rules);
}
+ /// Extend the list of syncing rules in the context with the given new rules.
+ pub fn add_sync_rules(&mut self, new_rules: I)
+ where
+ I: IntoIterator- ,
+ {
+ self.sync_rules.extend(new_rules);
+ }
+
+ /// Clears all sync rules for this InstanceContext
+ pub fn clear_sync_rules(&mut self) {
+ self.sync_rules.clear();
+ }
+
pub fn set_emit_legacy_scripts(&mut self, emit_legacy_scripts: bool) {
self.emit_legacy_scripts = emit_legacy_scripts;
}
+
+ /// Returns the middleware specified by the first sync rule that
+ /// matches the provided path. This does not handle default syncing rules.
+ pub fn get_user_sync_rule(&self, path: &Path) -> Option<&SyncRule> {
+ self.sync_rules.iter().find(|&rule| rule.matches(path))
+ }
}
impl Default for InstanceContext {
@@ -216,3 +241,64 @@ impl From<&Path> for InstigatingSource {
InstigatingSource::Path(path.to_path_buf())
}
}
+
+/// Represents an user-specified rule for transforming files
+/// into Instances using a given middleware.
+#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
+pub struct SyncRule {
+ /// A pattern used to determine if a file is included in this SyncRule
+ #[serde(rename = "pattern")]
+ pub include: Glob,
+ /// A pattern used to determine if a file is excluded from this SyncRule.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub exclude: Option,
+ /// The middleware specified by the user for this SyncRule
+ #[serde(rename = "use")]
+ pub middleware: Middleware,
+ /// A suffix to trim off of file names, including the file extension.
+ /// If not specified, the file extension is the only thing cut off.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub suffix: Option,
+ /// The 'base' of the glob above, allowing it to be used
+ /// relative to a path instead of absolute.
+ #[serde(skip)]
+ pub base_path: PathBuf,
+}
+
+impl SyncRule {
+ /// Returns whether the given path matches this rule.
+ pub fn matches(&self, path: &Path) -> bool {
+ match path.strip_prefix(&self.base_path) {
+ Ok(suffix) => {
+ if let Some(pattern) = &self.exclude {
+ if pattern.is_match(suffix) {
+ return false;
+ }
+ }
+ self.include.is_match(suffix)
+ }
+ Err(_) => false,
+ }
+ }
+
+ pub fn file_name_for_path<'a>(&self, path: &'a Path) -> anyhow::Result<&'a str> {
+ if let Some(suffix) = &self.suffix {
+ let file_name = path
+ .file_name()
+ .and_then(|s| s.to_str())
+ .with_context(|| format!("file name of {} is invalid", path.display()))?;
+ if file_name.ends_with(suffix) {
+ let end = file_name.len().saturating_sub(suffix.len());
+ Ok(&file_name[..end])
+ } else {
+ Ok(file_name)
+ }
+ } else {
+ // If the user doesn't specify a suffix, we assume they just want
+ // the name of the file (the file_stem)
+ path.file_stem()
+ .and_then(|s| s.to_str())
+ .with_context(|| format!("file name of {} is invalid", path.display()))
+ }
+ }
+}
diff --git a/src/snapshot_middleware/csv.rs b/src/snapshot_middleware/csv.rs
index 708f1bb8..52e8ab0f 100644
--- a/src/snapshot_middleware/csv.rs
+++ b/src/snapshot_middleware/csv.rs
@@ -10,16 +10,14 @@ use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
use super::{
dir::{dir_meta, snapshot_dir_no_meta},
meta_file::AdjacentMetadata,
- util::PathExt,
};
pub fn snapshot_csv(
_context: &InstanceContext,
vfs: &Vfs,
path: &Path,
+ name: &str,
) -> anyhow::Result