diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml
new file mode 100644
index 00000000..699152cc
--- /dev/null
+++ b/.github/workflows/changelog.yml
@@ -0,0 +1,23 @@
+name: Changelog Check
+
+on:
+ pull_request:
+ types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
+ branches:
+ - master
+
+jobs:
+ build:
+ name: Check Actions
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Changelog check
+ uses: Zomzog/changelog-checker@v1.3.0
+ with:
+ fileName: CHANGELOG.md
+ noChangelogLabel: skip changelog
+ checkNotification: Simple
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 80346f50..b0e9fefe 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -67,3 +67,4 @@ jobs:
- name: Clippy
run: cargo clippy
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b60a9bdb..a8e82bfb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,16 @@
* Skip confirming patches that contain only a datamodel name change. ([#688])
* Added sync reminder notification. ([#689])
* Added protection against syncing a model to a place. ([#691])
+* Select Instances from diff tree view ([#709])
+* Fix Rojo breaking when users undo/redo in Studio ([#708])
+* Improved sync info text on Connected page. ([#692])
+* Fix patch visualizer breaking when instances are removed during sync ([#713])
+* Patch visualizer now indicates what changes failed to apply. ([#717])
+* Add buttons for navigation on the Connected page ([#722])
+* Improve tooltip behavior ([#723])
+* Better settings controls ([#725])
+* Rework patch visualizer with many fixes and improvements ([#726])
+* Added support for syncing in `.toml` files ([#633])
[#631]: https://github.com/rojo-rbx/rojo/pull/631
[#668]: https://github.com/rojo-rbx/rojo/pull/668
@@ -16,6 +26,16 @@
[#688]: https://github.com/rojo-rbx/rojo/pull/688
[#689]: https://github.com/rojo-rbx/rojo/pull/689
[#691]: https://github.com/rojo-rbx/rojo/pull/691
+[#709]: https://github.com/rojo-rbx/rojo/pull/709
+[#708]: https://github.com/rojo-rbx/rojo/pull/708
+[#692]: https://github.com/rojo-rbx/rojo/pull/692
+[#713]: https://github.com/rojo-rbx/rojo/pull/713
+[#717]: https://github.com/rojo-rbx/rojo/pull/717
+[#722]: https://github.com/rojo-rbx/rojo/pull/722
+[#723]: https://github.com/rojo-rbx/rojo/pull/723
+[#725]: https://github.com/rojo-rbx/rojo/pull/725
+[#726]: https://github.com/rojo-rbx/rojo/pull/726
+[#633]: https://github.com/rojo-rbx/rojo/pull/633
## [7.3.0] - April 22, 2023
* Added `$attributes` to project format. ([#574])
@@ -581,4 +601,4 @@ This is a general maintenance release for the Rojo 0.5.x release series.
* More robust syncing with a new reconciler
## [0.1.0](https://github.com/rojo-rbx/rojo/releases/tag/v0.1.0) (November 29, 2017)
-* Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs)
\ No newline at end of file
+* Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs)
diff --git a/Cargo.lock b/Cargo.lock
index 12e9f860..cc544cf7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1308,9 +1308,9 @@ dependencies = [
[[package]]
name = "openssl"
-version = "0.10.51"
+version = "0.10.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97ea2d98598bf9ada7ea6ee8a30fb74f9156b63bbe495d64ec2b87c269d2dda3"
+checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
dependencies = [
"bitflags",
"cfg-if 1.0.0",
@@ -1349,9 +1349,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
-version = "0.9.86"
+version = "0.9.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "992bac49bdbab4423199c654a5515bd2a6c6a23bf03f2dd3bdb7e5ae6259bc69"
+checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
dependencies = [
"cc",
"libc",
@@ -1922,6 +1922,7 @@ dependencies = [
"termcolor",
"thiserror",
"tokio",
+ "toml",
"tracy-client 0.13.2",
"uuid",
"walkdir",
diff --git a/Cargo.toml b/Cargo.toml
index bbbbcbf9..124d5c8f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -80,6 +80,7 @@ ritz = "0.1.0"
roblox_install = "1.0.0"
serde = { version = "1.0.130", features = ["derive", "rc"] }
serde_json = "1.0.68"
+toml = "0.5.9"
termcolor = "1.1.2"
thiserror = "1.0.30"
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
diff --git a/plugin/src/ApiContext.lua b/plugin/src/ApiContext.lua
index 98890d4f..9a683dd5 100644
--- a/plugin/src/ApiContext.lua
+++ b/plugin/src/ApiContext.lua
@@ -11,13 +11,6 @@ local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
---[[
- Returns a promise that will never resolve nor reject.
-]]
-local function hangingPromise()
- return Promise.new(function() end)
-end
-
local function rejectFailedRequests(response)
if response.code >= 400 then
local message = string.format("HTTP %s:\n%s", tostring(response.code), response.body)
@@ -212,12 +205,8 @@ function ApiContext:retrieveMessages()
local function sendRequest()
local request = Http.get(url)
:catch(function(err)
- if err.type == Http.Error.Kind.Timeout then
- if self.__connected then
- return sendRequest()
- else
- return hangingPromise()
- end
+ if err.type == Http.Error.Kind.Timeout and self.__connected then
+ return sendRequest()
end
return Promise.reject(err)
diff --git a/plugin/src/App/Components/Checkbox.lua b/plugin/src/App/Components/Checkbox.lua
index 21ec56b3..aa351b1f 100644
--- a/plugin/src/App/Components/Checkbox.lua
+++ b/plugin/src/App/Components/Checkbox.lua
@@ -51,10 +51,15 @@ function Checkbox:render()
ZIndex = self.props.zIndex,
BackgroundTransparency = 1,
- [Roact.Event.Activated] = self.props.onClick,
+ [Roact.Event.Activated] = function()
+ if self.props.locked then return end
+ self.props.onClick()
+ end,
}, {
StateTip = e(Tooltip.Trigger, {
- text = if self.props.active then "Enabled" else "Disabled",
+ text =
+ (if self.props.locked then "[LOCKED] " else "")
+ .. (if self.props.active then "Enabled" else "Disabled"),
}),
Active = e(SlicedImage, {
@@ -65,7 +70,7 @@ function Checkbox:render()
zIndex = 2,
}, {
Icon = e("ImageLabel", {
- Image = Assets.Images.Checkbox.Active,
+ Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Active,
ImageColor3 = theme.Active.IconColor,
ImageTransparency = activeTransparency,
@@ -84,7 +89,7 @@ function Checkbox:render()
size = UDim2.new(1, 0, 1, 0),
}, {
Icon = e("ImageLabel", {
- Image = Assets.Images.Checkbox.Inactive,
+ Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Inactive,
ImageColor3 = theme.Inactive.IconColor,
ImageTransparency = self.props.transparency,
diff --git a/plugin/src/App/Components/Dropdown.lua b/plugin/src/App/Components/Dropdown.lua
index ef9a4211..21a60908 100644
--- a/plugin/src/App/Components/Dropdown.lua
+++ b/plugin/src/App/Components/Dropdown.lua
@@ -29,7 +29,13 @@ function Dropdown:init()
})
end
-function Dropdown:didUpdate()
+function Dropdown:didUpdate(prevProps)
+ if self.props.locked and not prevProps.locked then
+ self:setState({
+ open = false,
+ })
+ end
+
self.openMotor:setGoal(
Flipper.Spring.new(self.state.open and 1 or 0, {
frequency = 6,
@@ -68,6 +74,7 @@ function Dropdown:render()
Font = Enum.Font.GothamMedium,
[Roact.Event.Activated] = function()
+ if self.props.locked then return end
self:setState({
open = false,
})
@@ -89,6 +96,7 @@ function Dropdown:render()
BackgroundTransparency = 1,
[Roact.Event.Activated] = function()
+ if self.props.locked then return end
self:setState({
open = not self.state.open,
})
@@ -101,7 +109,7 @@ function Dropdown:render()
size = UDim2.new(1, 0, 1, 0),
}, {
DropArrow = e("ImageLabel", {
- Image = Assets.Images.Dropdown.Arrow,
+ 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),
diff --git a/plugin/src/App/Components/PatchVisualizer/ChangeList.lua b/plugin/src/App/Components/PatchVisualizer/ChangeList.lua
index beb0d327..ed6da37a 100644
--- a/plugin/src/App/Components/PatchVisualizer/ChangeList.lua
+++ b/plugin/src/App/Components/PatchVisualizer/ChangeList.lua
@@ -8,6 +8,8 @@ local Theme = require(Plugin.App.Theme)
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
local DisplayValue = require(script.Parent.DisplayValue)
+local EMPTY_TABLE = {}
+
local e = Roact.createElement
local ChangeList = Roact.Component:extend("ChangeList")
@@ -26,8 +28,6 @@ function ChangeList:render()
return 0.93 + (0.07 * t)
end)
- local columnVisibility = props.columnVisibility
-
local rows = {}
local pad = {
PaddingLeft = UDim.new(0, 5),
@@ -48,7 +48,6 @@ function ChangeList:render()
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
A = e("TextLabel", {
- Visible = columnVisibility[1],
Text = tostring(changes[1][1]),
BackgroundTransparency = 1,
Font = Enum.Font.GothamBold,
@@ -61,7 +60,6 @@ function ChangeList:render()
LayoutOrder = 1,
}),
B = e("TextLabel", {
- Visible = columnVisibility[2],
Text = tostring(changes[1][2]),
BackgroundTransparency = 1,
Font = Enum.Font.GothamBold,
@@ -74,7 +72,6 @@ function ChangeList:render()
LayoutOrder = 2,
}),
C = e("TextLabel", {
- Visible = columnVisibility[3],
Text = tostring(changes[1][3]),
BackgroundTransparency = 1,
Font = Enum.Font.GothamBold,
@@ -93,6 +90,9 @@ function ChangeList:render()
continue -- Skip headers, already handled above
end
+ local metadata = values[4] or EMPTY_TABLE
+ local isWarning = metadata.isWarning
+
rows[row] = e("Frame", {
Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
@@ -108,12 +108,11 @@ function ChangeList:render()
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
A = e("TextLabel", {
- Visible = columnVisibility[1],
- Text = tostring(values[1]),
+ Text = (if isWarning then "⚠ " else "") .. tostring(values[1]),
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
- TextColor3 = theme.Settings.Setting.DescriptionColor,
+ TextColor3 = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
@@ -123,7 +122,6 @@ function ChangeList:render()
B = e(
"Frame",
{
- Visible = columnVisibility[2],
BackgroundTransparency = 1,
Size = UDim2.new(0.35, 0, 1, 0),
LayoutOrder = 2,
@@ -131,12 +129,12 @@ function ChangeList:render()
e(DisplayValue, {
value = values[2],
transparency = props.transparency,
+ textColor = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
})
),
C = e(
"Frame",
{
- Visible = columnVisibility[3],
BackgroundTransparency = 1,
Size = UDim2.new(0.35, 0, 1, 0),
LayoutOrder = 3,
@@ -144,6 +142,7 @@ function ChangeList:render()
e(DisplayValue, {
value = values[3],
transparency = props.transparency,
+ textColor = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
})
),
})
diff --git a/plugin/src/App/Components/PatchVisualizer/DisplayValue.lua b/plugin/src/App/Components/PatchVisualizer/DisplayValue.lua
index 891ec9f2..3cf1a11a 100644
--- a/plugin/src/App/Components/PatchVisualizer/DisplayValue.lua
+++ b/plugin/src/App/Components/PatchVisualizer/DisplayValue.lua
@@ -34,7 +34,7 @@ local function DisplayValue(props)
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
- TextColor3 = theme.Settings.Setting.DescriptionColor,
+ TextColor3 = props.textColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
@@ -78,7 +78,7 @@ local function DisplayValue(props)
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
- TextColor3 = theme.Settings.Setting.DescriptionColor,
+ TextColor3 = props.textColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
@@ -95,7 +95,7 @@ local function DisplayValue(props)
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
- TextColor3 = theme.Settings.Setting.DescriptionColor,
+ TextColor3 = props.textColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
diff --git a/plugin/src/App/Components/PatchVisualizer/DomLabel.lua b/plugin/src/App/Components/PatchVisualizer/DomLabel.lua
index 9f967c32..facf70cd 100644
--- a/plugin/src/App/Components/PatchVisualizer/DomLabel.lua
+++ b/plugin/src/App/Components/PatchVisualizer/DomLabel.lua
@@ -1,3 +1,4 @@
+local SelectionService = game:GetService("Selection")
local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo")
@@ -14,6 +15,7 @@ local bindingUtil = require(Plugin.App.bindingUtil)
local e = Roact.createElement
local ChangeList = require(script.Parent.ChangeList)
+local Tooltip = require(script.Parent.Parent.Tooltip)
local Expansion = Roact.Component:extend("Expansion")
@@ -32,7 +34,6 @@ function Expansion:render()
ChangeList = e(ChangeList, {
changes = props.changeList,
transparency = props.transparency,
- columnVisibility = props.columnVisibility,
}),
})
end
@@ -69,6 +70,22 @@ function DomLabel:init()
end)
end
+function DomLabel:didUpdate(prevProps)
+ if
+ prevProps.instance ~= self.props.instance
+ or prevProps.patchType ~= self.props.patchType
+ or prevProps.name ~= self.props.name
+ or prevProps.changeList ~= self.props.changeList
+ then
+ -- Close the expansion when the domlabel is changed to a different thing
+ self.expanded = false
+ self.motor:setGoal(Flipper.Spring.new(30, {
+ frequency = 5,
+ dampingRatio = 1,
+ }))
+ end
+end
+
function DomLabel:render()
local props = self.props
@@ -106,28 +123,53 @@ function DomLabel:render()
PaddingLeft = UDim.new(0, 10),
PaddingRight = UDim.new(0, 10),
}),
- ExpandButton = if props.changeList
- then e("TextButton", {
- BackgroundTransparency = 1,
- Text = "",
- Size = UDim2.new(1, 0, 1, 0),
- [Roact.Event.Activated] = function()
- self.expanded = not self.expanded
- local goalHeight = 30 + (if self.expanded then math.clamp(#self.props.changeList * 30, 30, 30 * 6) else 0)
- self.motor:setGoal(Flipper.Spring.new(goalHeight, {
- frequency = 5,
- dampingRatio = 1,
- }))
- end,
- })
- else nil,
+ Button = e("TextButton", {
+ BackgroundTransparency = 1,
+ Text = "",
+ Size = UDim2.new(1, 0, 1, 0),
+ [Roact.Event.Activated] = function(_rbx: Instance, _input: InputObject, clickCount: number)
+ if clickCount == 1 then
+ -- Double click opens the instance in explorer
+ self.lastDoubleClickTime = os.clock()
+ if props.instance then
+ SelectionService:Set({ props.instance })
+ end
+ elseif clickCount == 0 then
+ -- Single click expands the changes
+ task.wait(0.25)
+ if os.clock() - (self.lastDoubleClickTime or 0) <= 0.25 then
+ -- This is a double click, so don't expand
+ return
+ end
+
+ if props.changeList then
+ self.expanded = not self.expanded
+ local goalHeight = 30
+ + (if self.expanded then math.clamp(#props.changeList * 30, 30, 30 * 6) else 0)
+ self.motor:setGoal(Flipper.Spring.new(goalHeight, {
+ frequency = 5,
+ dampingRatio = 1,
+ }))
+ end
+ end
+ end,
+ }, {
+ StateTip = if (props.instance or props.changeList)
+ then e(Tooltip.Trigger, {
+ text = (if props.changeList
+ then "Click to " .. (if self.expanded then "hide" else "view") .. " changes"
+ else "") .. (if props.instance
+ then (if props.changeList then " & d" else "D") .. "ouble click to open in Explorer"
+ else ""),
+ })
+ else nil,
+ }),
Expansion = if props.changeList
then e(Expansion, {
rendered = self.state.renderExpansion,
indent = indent,
transparency = props.transparency,
changeList = props.changeList,
- columnVisibility = props.columnVisibility,
})
else nil,
DiffIcon = if props.patchType
@@ -152,7 +194,7 @@ function DomLabel:render()
AnchorPoint = Vector2.new(0, 0.5),
}),
InstanceName = e("TextLabel", {
- Text = props.name .. (props.hint and string.format(
+ Text = (if props.isWarning then "⚠ " else "") .. props.name .. (props.hint and string.format(
' %s',
theme.AddressEntry.PlaceholderColor:ToHex(),
props.hint
@@ -161,7 +203,7 @@ function DomLabel:render()
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
- TextColor3 = theme.Settings.Setting.DescriptionColor,
+ TextColor3 = if props.isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
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 95673dfe..73af4502 100644
--- a/plugin/src/App/Components/PatchVisualizer/init.lua
+++ b/plugin/src/App/Components/PatchVisualizer/init.lua
@@ -1,143 +1,18 @@
-local HttpService = game:GetService("HttpService")
-
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
-local Log = require(Packages.Log)
+local PatchTree = require(Plugin.PatchTree)
local PatchSet = require(Plugin.PatchSet)
-local decodeValue = require(Plugin.Reconciler.decodeValue)
-local getProperty = require(Plugin.Reconciler.getProperty)
+local Theme = require(Plugin.App.Theme)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
local e = Roact.createElement
-local function alphabeticalNext(t, state)
- -- Equivalent of the next function, but returns the keys in the alphabetic
- -- order of node names. We use a temporary ordered key table that is stored in the
- -- table being iterated.
-
- local key = nil
- if state == nil then
- -- First iteration, generate the index
- local orderedIndex, i = table.create(5), 0
- for k in t do
- i += 1
- orderedIndex[i] = k
- end
- table.sort(orderedIndex, function(a, b)
- local nodeA, nodeB = t[a], t[b]
- return (nodeA.name or "") < (nodeB.name or "")
- end)
-
- t.__orderedIndex = orderedIndex
- key = orderedIndex[1]
- else
- -- Fetch the next value
- for i, orderedState in t.__orderedIndex do
- if orderedState == state then
- key = t.__orderedIndex[i + 1]
- break
- end
- end
- end
-
- if key then
- return key, t[key]
- end
-
- -- No more value to return, cleanup
- t.__orderedIndex = nil
- return
-end
-
-local function alphabeticalPairs(t)
- -- Equivalent of the pairs() iterator, but sorted
- return alphabeticalNext, t, nil
-end
-
-local function Tree()
- local tree = {
- idToNode = {},
- ROOT = {
- className = "DataModel",
- name = "ROOT",
- children = {},
- },
- }
- -- Add ROOT to idToNode or it won't be found by getNode since that searches *within* ROOT
- tree.idToNode["ROOT"] = tree.ROOT
-
- function tree:getNode(id, target)
- if self.idToNode[id] then
- return self.idToNode[id]
- end
-
- for nodeId, node in target or tree.ROOT.children do
- if nodeId == id then
- self.idToNode[id] = node
- return node
- end
- local descendant = self:getNode(id, node.children)
- if descendant then
- return descendant
- end
- end
-
- return nil
- end
-
- function tree:addNode(parent, props)
- parent = parent or "ROOT"
-
- local node = self:getNode(props.id)
- if node then
- for k, v in props do
- node[k] = v
- end
- return node
- end
-
- node = table.clone(props)
- node.children = {}
-
- local parentNode = self:getNode(parent)
- if not parentNode then
- Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
- return
- end
-
- parentNode.children[node.id] = node
- self.idToNode[node.id] = node
-
- return node
- end
-
- function tree:buildAncestryNodes(ancestry, patch, instanceMap)
- -- Build nodes for ancestry by going up the tree
- local previousId = "ROOT"
- for _, ancestorId in ancestry do
- local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
- if not value then
- Log.warn("Failed to find ancestor object for " .. ancestorId)
- continue
- end
- self:addNode(previousId, {
- id = ancestorId,
- className = value.ClassName,
- name = value.Name,
- })
- previousId = ancestorId
- end
- end
-
- return tree
-end
-
local DomLabel = require(script.DomLabel)
local PatchVisualizer = Roact.Component:extend("PatchVisualizer")
@@ -153,250 +28,90 @@ function PatchVisualizer:willUnmount()
end
function PatchVisualizer:shouldUpdate(nextProps)
+ if self.props.patchTree ~= nextProps.patchTree then
+ return true
+ end
+
local currentPatch, nextPatch = self.props.patch, nextProps.patch
-
- return not PatchSet.isEqual(currentPatch, nextPatch)
-end
-
-function PatchVisualizer:buildTree(patch, instanceMap)
- local tree = Tree()
-
- for _, change in patch.updated do
- local instance = instanceMap.fromIds[change.id]
- if not instance then
- continue
- end
-
- -- Gather ancestors from existing DOM
- local ancestry = {}
- local parentObject = instance.Parent
- local parentId = instanceMap.fromInstances[parentObject]
- while parentObject do
- table.insert(ancestry, 1, parentId)
- parentObject = parentObject.Parent
- parentId = instanceMap.fromInstances[parentObject]
- end
-
- tree:buildAncestryNodes(ancestry, patch, instanceMap)
-
- -- Gather detail text
- local changeList, hint = nil, nil
- if next(change.changedProperties) or change.changedName then
- changeList = {}
-
- local hintBuffer, i = {}, 0
- local function addProp(prop: string, current: any?, incoming: any?)
- i += 1
- hintBuffer[i] = prop
- changeList[i] = { prop, current, incoming }
- end
-
- -- Gather the changes
-
- if change.changedName then
- addProp("Name", instance.Name, change.changedName)
- end
-
- for prop, incoming in change.changedProperties do
- local incomingSuccess, incomingValue = decodeValue(incoming, instanceMap)
- local currentSuccess, currentValue = getProperty(instance, prop)
-
- addProp(
- prop,
- if currentSuccess then currentValue else "[Error]",
- if incomingSuccess then incomingValue else next(incoming)
- )
- end
-
- -- Finalize detail values
-
- -- Trim hint to top 3
- table.sort(hintBuffer)
- if #hintBuffer > 3 then
- hintBuffer = {
- hintBuffer[1],
- hintBuffer[2],
- hintBuffer[3],
- i - 3 .. " more",
- }
- end
- hint = table.concat(hintBuffer, ", ")
-
- -- Sort changes and add header
- table.sort(changeList, function(a, b)
- return a[1] < b[1]
- end)
- table.insert(changeList, 1, { "Property", "Current", "Incoming" })
- end
-
- -- Add this node to tree
- tree:addNode(instanceMap.fromInstances[instance.Parent], {
- id = change.id,
- patchType = "Edit",
- className = instance.ClassName,
- name = instance.Name,
- hint = hint,
- changeList = changeList,
- })
+ if currentPatch ~= nil or nextPatch ~= nil then
+ return not PatchSet.isEqual(currentPatch, nextPatch)
end
- for _, instance in patch.removed do
- -- Gather ancestors from existing DOM
- -- (note that they may have no ID if they're being removed as unknown)
- local ancestry = {}
- local parentObject = instance.Parent
- local parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
- while parentObject do
- instanceMap:insert(parentId, parentObject)
- table.insert(ancestry, 1, parentId)
- parentObject = parentObject.Parent
- parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
- end
-
- tree:buildAncestryNodes(ancestry, patch, instanceMap)
-
- -- Add this node to tree
- local nodeId = instanceMap.fromInstances[instance] or HttpService:GenerateGUID(false)
- instanceMap:insert(nodeId, instance)
- tree:addNode(instanceMap.fromInstances[instance.Parent], {
- id = nodeId,
- patchType = "Remove",
- className = instance.ClassName,
- name = instance.Name,
- })
- end
-
- for _, change in patch.added do
- -- Gather ancestors from existing DOM or future additions
- local ancestry = {}
- local parentId = change.Parent
- local parentData = patch.added[parentId]
- local parentObject = instanceMap.fromIds[parentId]
- while parentId do
- table.insert(ancestry, 1, parentId)
- parentId = nil
-
- if parentData then
- parentId = parentData.Parent
- parentData = patch.added[parentId]
- parentObject = instanceMap.fromIds[parentId]
- elseif parentObject then
- parentObject = parentObject.Parent
- parentId = instanceMap.fromInstances[parentObject]
- parentData = patch.added[parentId]
- end
- end
-
- tree:buildAncestryNodes(ancestry, patch, instanceMap)
-
- -- Gather detail text
- local changeList, hint = nil, nil
- if next(change.Properties) then
- changeList = {}
-
- local hintBuffer, i = {}, 0
- for prop, incoming in change.Properties do
- i += 1
- hintBuffer[i] = prop
-
- local success, incomingValue = decodeValue(incoming, instanceMap)
- if success then
- table.insert(changeList, { prop, "N/A", incomingValue })
- else
- table.insert(changeList, { prop, "N/A", next(incoming) })
- end
- end
-
- -- Finalize detail values
-
- -- Trim hint to top 3
- table.sort(hintBuffer)
- if #hintBuffer > 3 then
- hintBuffer = {
- hintBuffer[1],
- hintBuffer[2],
- hintBuffer[3],
- i - 3 .. " more",
- }
- end
- hint = table.concat(hintBuffer, ", ")
-
- -- Sort changes and add header
- table.sort(changeList, function(a, b)
- return a[1] < b[1]
- end)
- table.insert(changeList, 1, { "Property", "Current", "Incoming" })
- end
-
- -- Add this node to tree
- tree:addNode(change.Parent, {
- id = change.Id,
- patchType = "Add",
- className = change.ClassName,
- name = change.Name,
- hint = hint,
- changeList = changeList,
- })
- end
-
- return tree
+ return false
end
function PatchVisualizer:render()
- local patch = self.props.patch
- local instanceMap = self.props.instanceMap
-
- local tree = self:buildTree(patch, instanceMap)
+ local patchTree = self.props.patchTree
+ if patchTree == nil and self.props.patch ~= nil then
+ patchTree = PatchTree.build(self.props.patch, self.props.instanceMap, self.props.changeListHeaders or { "Property", "Current", "Incoming" })
+ if self.props.unappliedPatch then
+ patchTree = PatchTree.updateMetadata(patchTree, self.props.patch, self.props.instanceMap, self.props.unappliedPatch)
+ end
+ end
-- Recusively draw tree
local scrollElements, elementHeights = {}, {}
- local function drawNode(node, depth)
- local elementHeight, setElementHeight = Roact.createBinding(30)
- table.insert(elementHeights, elementHeight)
- table.insert(
- scrollElements,
- e(DomLabel, {
- columnVisibility = self.props.columnVisibility,
- updateEvent = self.updateEvent,
- elementHeight = elementHeight,
- setElementHeight = setElementHeight,
- patchType = node.patchType,
- className = node.className,
- name = node.name,
- hint = node.hint,
- changeList = node.changeList,
- depth = depth,
- transparency = self.props.transparency,
- })
- )
- for _, childNode in alphabeticalPairs(node.children) do
- drawNode(childNode, depth + 1)
+ if patchTree then
+ local function drawNode(node, depth)
+ local elementHeight, setElementHeight = Roact.createBinding(30)
+ table.insert(elementHeights, elementHeight)
+ table.insert(
+ scrollElements,
+ e(DomLabel, {
+ updateEvent = self.updateEvent,
+ elementHeight = elementHeight,
+ setElementHeight = setElementHeight,
+ patchType = node.patchType,
+ className = node.className,
+ isWarning = node.isWarning,
+ instance = node.instance,
+ name = node.name,
+ hint = node.hint,
+ changeList = node.changeList,
+ depth = depth,
+ transparency = self.props.transparency,
+ })
+ )
end
- end
- for _, node in alphabeticalPairs(tree.ROOT.children) do
- drawNode(node, 0)
+
+ patchTree:forEach(function(node, depth)
+ drawNode(node, depth)
+ end)
end
- return e(BorderedContainer, {
- transparency = self.props.transparency,
- size = self.props.size,
- position = self.props.position,
- layoutOrder = self.props.layoutOrder,
- }, {
- VirtualScroller = e(VirtualScroller, {
- size = UDim2.new(1, 0, 1, 0),
+ return Theme.with(function(theme)
+ return e(BorderedContainer, {
transparency = self.props.transparency,
- count = #scrollElements,
- updateEvent = self.updateEvent.Event,
- render = function(i)
- return scrollElements[i]
- end,
- getHeightBinding = function(i)
- return elementHeights[i]
- end,
- }),
- })
+ size = self.props.size,
+ position = self.props.position,
+ layoutOrder = self.props.layoutOrder,
+ }, {
+ CleanMerge = e("TextLabel", {
+ Visible = #scrollElements == 0,
+ Text = "No changes to sync, project is up to date.",
+ Font = Enum.Font.GothamMedium,
+ TextSize = 15,
+ TextColor3 = theme.Settings.Setting.DescriptionColor,
+ TextWrapped = true,
+ Size = UDim2.new(1, 0, 1, 0),
+ BackgroundTransparency = 1,
+ }),
+
+ VirtualScroller = e(VirtualScroller, {
+ size = UDim2.new(1, 0, 1, 0),
+ transparency = self.props.transparency,
+ count = #scrollElements,
+ updateEvent = self.updateEvent.Event,
+ render = function(i)
+ return scrollElements[i]
+ end,
+ getHeightBinding = function(i)
+ return elementHeights[i]
+ end,
+ }),
+ })
+ end)
end
return PatchVisualizer
diff --git a/plugin/src/App/Components/Tooltip.lua b/plugin/src/App/Components/Tooltip.lua
index bc3443d5..9c1fb151 100644
--- a/plugin/src/App/Components/Tooltip.lua
+++ b/plugin/src/App/Components/Tooltip.lua
@@ -165,46 +165,94 @@ function Trigger:init()
self.id = HttpService:GenerateGUID(false)
self.ref = Roact.createRef()
self.mousePos = Vector2.zero
+ self.showingPopup = false
self.destroy = function()
self.props.context.removeTip(self.id)
+ self.showingPopup = false
end
end
function Trigger:willUnmount()
if self.showDelayThread then
- task.cancel(self.showDelayThread)
+ pcall(task.cancel, self.showDelayThread)
end
if self.destroy then
self.destroy()
end
end
+function Trigger:didUpdate(prevProps)
+ if prevProps.text ~= self.props.text then
+ -- Any existing popup is now invalid
+ self.props.context.removeTip(self.id)
+ self.showingPopup = false
+
+ -- Let the new text propagate
+ self:managePopup()
+ end
+end
+
+function Trigger:isHovering()
+ local rbx = self.ref.current
+ if rbx then
+ local pos = rbx.AbsolutePosition
+ local size = rbx.AbsoluteSize
+ local mousePos = self.mousePos
+
+ return
+ mousePos.X >= pos.X and mousePos.X <= pos.X + size.X
+ and mousePos.Y >= pos.Y and mousePos.Y <= pos.Y + size.Y
+ end
+ return false
+end
+
+function Trigger:managePopup()
+ if self:isHovering() then
+ if self.showingPopup or self.showDelayThread then
+ -- Don't duplicate popups
+ return
+ end
+
+ self.showDelayThread = task.delay(DELAY, function()
+ self.props.context.addTip(self.id, {
+ Text = self.props.text,
+ Position = self.mousePos,
+ Trigger = self.ref,
+ })
+ self.showDelayThread = nil
+ self.showingPopup = true
+ end)
+ else
+ if self.showDelayThread then
+ pcall(task.cancel, self.showDelayThread)
+ self.showDelayThread = nil
+ end
+ self.props.context.removeTip(self.id)
+ self.showingPopup = false
+ end
+end
+
function Trigger:render()
+ local function recalculate(rbx)
+ local widget = rbx:FindFirstAncestorOfClass("DockWidgetPluginGui")
+ if not widget then return end
+ self.mousePos = widget:GetRelativeMousePosition()
+
+ self:managePopup()
+ end
+
return e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
ZIndex = self.props.zIndex or 100,
[Roact.Ref] = self.ref,
- [Roact.Event.MouseMoved] = function(_rbx, x, y)
- self.mousePos = Vector2.new(x, y)
- end,
- [Roact.Event.MouseEnter] = function()
- self.showDelayThread = task.delay(DELAY, function()
- self.props.context.addTip(self.id, {
- Text = self.props.text,
- Position = self.mousePos,
- Trigger = self.ref,
- })
- end)
- end,
- [Roact.Event.MouseLeave] = function()
- if self.showDelayThread then
- task.cancel(self.showDelayThread)
- end
- self.props.context.removeTip(self.id)
- end,
+ [Roact.Change.AbsolutePosition] = recalculate,
+ [Roact.Change.AbsoluteSize] = recalculate,
+ [Roact.Event.MouseMoved] = recalculate,
+ [Roact.Event.MouseLeave] = recalculate,
+ [Roact.Event.MouseEnter] = recalculate,
})
end
diff --git a/plugin/src/App/StatusPages/Confirming.lua b/plugin/src/App/StatusPages/Confirming.lua
index 3359eaf6..a5592347 100644
--- a/plugin/src/App/StatusPages/Confirming.lua
+++ b/plugin/src/App/StatusPages/Confirming.lua
@@ -52,7 +52,7 @@ function ConfirmingPage:render()
transparency = self.props.transparency,
layoutOrder = 3,
- columnVisibility = {true, true, true},
+ changeListHeaders = { "Property", "Current", "Incoming" },
patch = self.props.confirmData.patch,
instanceMap = self.props.confirmData.instanceMap,
}),
diff --git a/plugin/src/App/StatusPages/Connected.lua b/plugin/src/App/StatusPages/Connected.lua
index af43e6a2..00f6822b 100644
--- a/plugin/src/App/StatusPages/Connected.lua
+++ b/plugin/src/App/StatusPages/Connected.lua
@@ -12,6 +12,7 @@ local PatchSet = require(Plugin.PatchSet)
local Header = require(Plugin.App.Components.Header)
local IconButton = require(Plugin.App.Components.IconButton)
+local TextButton = require(Plugin.App.Components.TextButton)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local Tooltip = require(Plugin.App.Components.Tooltip)
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
@@ -55,7 +56,7 @@ function ChangesDrawer:render()
return e(BorderedContainer, {
transparency = self.props.transparency,
size = self.props.height:map(function(y)
- return UDim2.new(1, 0, y, -180 * y)
+ return UDim2.new(1, 0, y, -220 * y)
end),
position = UDim2.new(0, 0, 1, 0),
anchorPoint = Vector2.new(0, 1),
@@ -82,9 +83,7 @@ function ChangesDrawer:render()
transparency = self.props.transparency,
layoutOrder = 3,
- columnVisibility = { true, false, true },
- patch = self.props.patch,
- instanceMap = self.serveSession.__instanceMap,
+ patchTree = self.props.patchTree,
}),
})
end)
@@ -137,22 +136,6 @@ local function ConnectionDetails(props)
}),
}),
- Disconnect = e(IconButton, {
- icon = Assets.Images.Icons.Close,
- iconSize = 24,
- color = theme.ConnectionDetails.DisconnectColor,
- transparency = props.transparency,
-
- position = UDim2.new(1, 0, 0.5, 0),
- anchorPoint = Vector2.new(1, 0.5),
-
- onClick = props.onDisconnect,
- }, {
- Tip = e(Tooltip.Trigger, {
- text = "Disconnect from the Rojo sync server",
- }),
- }),
-
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 17),
PaddingRight = UDim.new(0, 15),
@@ -170,9 +153,56 @@ function ConnectedPage:getChangeInfoText()
end
local elapsed = os.time() - patchData.timestamp
- local changes = PatchSet.countChanges(patchData.patch)
+ local unapplied = PatchSet.countChanges(patchData.unapplied)
- return string.format("Synced %d change%s %s", changes, changes == 1 and "" or "s", timeSinceText(elapsed))
+ return
+ "Synced "
+ .. timeSinceText(elapsed)
+ .. (if unapplied > 0 then
+ string.format(
+ ", but %d change%s failed to apply",
+ unapplied,
+ unapplied == 1 and "" or "s"
+ )
+ else "")
+ .. ""
+end
+
+function ConnectedPage:startChangeInfoTextUpdater()
+ -- Cancel any existing updater
+ self:stopChangeInfoTextUpdater()
+
+ -- Start a new updater
+ self.changeInfoTextUpdater = task.defer(function()
+ while true do
+ if self.state.hoveringChangeInfo then
+ self.setChangeInfoText("" .. self:getChangeInfoText() .. "")
+ else
+ self.setChangeInfoText(self:getChangeInfoText())
+ end
+
+ local elapsed = os.time() - self.props.patchData.timestamp
+ local updateInterval = 1
+
+ -- Update timestamp text as frequently as currently needed
+ for _, UnitData in ipairs(AGE_UNITS) do
+ local UnitSeconds = UnitData[1]
+ if elapsed > UnitSeconds then
+ updateInterval = UnitSeconds
+ break
+ end
+ end
+
+ task.wait(updateInterval)
+ end
+ end)
+end
+
+function ConnectedPage:stopChangeInfoTextUpdater()
+ if self.changeInfoTextUpdater then
+ task.cancel(self.changeInfoTextUpdater)
+ self.changeInfoTextUpdater = nil
+ end
end
function ConnectedPage:init()
@@ -195,34 +225,21 @@ function ConnectedPage:init()
self:setState({
renderChanges = false,
+ hoveringChangeInfo = false,
})
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
- self.changeInfoTextUpdater = task.defer(function()
- while true do
- self.setChangeInfoText(self:getChangeInfoText())
-
- local elapsed = os.time() - self.props.patchData.timestamp
- local updateInterval = 1
-
- -- Update timestamp text as frequently as currently needed
- for _, UnitData in ipairs(AGE_UNITS) do
- local UnitSeconds = UnitData[1]
- if elapsed >= UnitSeconds then
- updateInterval = UnitSeconds
- break
- end
- end
-
- task.wait(updateInterval)
- end
- end)
+ self:startChangeInfoTextUpdater()
end
function ConnectedPage:willUnmount()
- if self.changeInfoTextUpdater then
- task.cancel(self.changeInfoTextUpdater)
+ self:stopChangeInfoTextUpdater()
+end
+
+function ConnectedPage:didUpdate(previousProps)
+ if self.props.patchData.timestamp ~= previousProps.patchData.timestamp then
+ self:startChangeInfoTextUpdater()
end
end
@@ -255,6 +272,44 @@ function ConnectedPage:render()
onDisconnect = self.props.onDisconnect,
}),
+ Buttons = e("Frame", {
+ Size = UDim2.new(1, 0, 0, 34),
+ LayoutOrder = 3,
+ BackgroundTransparency = 1,
+ ZIndex = 2,
+ }, {
+ Settings = e(TextButton, {
+ text = "Settings",
+ style = "Bordered",
+ transparency = self.props.transparency,
+ layoutOrder = 1,
+ onClick = self.props.onNavigateSettings,
+ }, {
+ Tip = e(Tooltip.Trigger, {
+ text = "View and modify plugin settings"
+ }),
+ }),
+
+ Disconnect = e(TextButton, {
+ text = "Disconnect",
+ style = "Solid",
+ transparency = self.props.transparency,
+ layoutOrder = 2,
+ onClick = self.props.onDisconnect,
+ }, {
+ Tip = e(Tooltip.Trigger, {
+ text = "Disconnect from the Rojo sync server"
+ }),
+ }),
+
+ Layout = e("UIListLayout", {
+ HorizontalAlignment = Enum.HorizontalAlignment.Right,
+ FillDirection = Enum.FillDirection.Horizontal,
+ SortOrder = Enum.SortOrder.LayoutOrder,
+ Padding = UDim.new(0, 10),
+ }),
+ }),
+
ChangeInfo = e("TextButton", {
Text = self.changeInfoText,
Font = Enum.Font.Gotham,
@@ -268,9 +323,23 @@ function ConnectedPage:render()
Size = UDim2.new(1, 0, 0, 28),
- LayoutOrder = 3,
+ LayoutOrder = 4,
BackgroundTransparency = 1,
+ [Roact.Event.MouseEnter] = function()
+ self:setState({
+ hoveringChangeInfo = true,
+ })
+ self.setChangeInfoText("" .. self:getChangeInfoText() .. "")
+ end,
+
+ [Roact.Event.MouseLeave] = function()
+ self:setState({
+ hoveringChangeInfo = false,
+ })
+ self.setChangeInfoText(self:getChangeInfoText())
+ end,
+
[Roact.Event.Activated] = function()
if self.state.renderChanges then
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
@@ -284,15 +353,19 @@ function ConnectedPage:render()
}))
end
end,
+ }, {
+ Tooltip = e(Tooltip.Trigger, {
+ text = if self.state.renderChanges then "Hide the changes" else "View the changes",
+ }),
}),
ChangesDrawer = e(ChangesDrawer, {
rendered = self.state.renderChanges,
transparency = self.props.transparency,
- patch = self.props.patchData.patch,
+ patchTree = self.props.patchTree,
serveSession = self.props.serveSession,
height = self.changeDrawerHeight,
- layoutOrder = 4,
+ layoutOrder = 5,
onClose = function()
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
diff --git a/plugin/src/App/StatusPages/Settings/Setting.lua b/plugin/src/App/StatusPages/Settings/Setting.lua
index af2dc0a2..4ce50de6 100644
--- a/plugin/src/App/StatusPages/Settings/Setting.lua
+++ b/plugin/src/App/StatusPages/Settings/Setting.lua
@@ -72,6 +72,7 @@ function Setting:render()
self.props.customInput
elseif self.props.options ~= nil then
e(Dropdown, {
+ locked = self.props.locked,
options = self.props.options,
active = self.state.setting,
transparency = self.props.transparency,
@@ -83,6 +84,7 @@ function Setting:render()
})
else
e(Checkbox, {
+ locked = self.props.locked,
active = self.state.setting,
transparency = self.props.transparency,
position = UDim2.new(1, 0, 0.5, 0),
@@ -111,12 +113,13 @@ function Setting:render()
BackgroundTransparency = 1,
}, {
Name = e("TextLabel", {
- Text = self.props.name,
+ Text = (if self.props.experimental then "⚠ " else "") .. self.props.name,
Font = Enum.Font.GothamBold,
TextSize = 17,
TextColor3 = theme.Setting.NameColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
+ RichText = true,
Size = UDim2.new(1, 0, 0, 17),
@@ -125,7 +128,7 @@ function Setting:render()
}),
Description = e("TextLabel", {
- Text = self.props.description,
+ Text = (if self.props.experimental then "[Experimental] " else "") .. self.props.description,
Font = Enum.Font.Gotham,
LineHeight = 1.2,
TextSize = 14,
@@ -133,11 +136,13 @@ function Setting:render()
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency,
TextWrapped = true,
+ RichText = true,
Size = self.containerSize:map(function(value)
+ local desc = (if self.props.experimental then "[Experimental] " else "") .. self.props.description
local offset = (self.props.onReset and 34 or 0) + (self.props.options ~= nil and 120 or 40)
local textBounds = getTextBounds(
- self.props.description, 14, Enum.Font.Gotham, 1.2,
+ desc, 14, Enum.Font.Gotham, 1.2,
Vector2.new(value.X - offset, math.huge)
)
return UDim2.new(1, -offset, 0, textBounds.Y)
diff --git a/plugin/src/App/StatusPages/Settings/init.lua b/plugin/src/App/StatusPages/Settings/init.lua
index eaa4cc85..b0900c54 100644
--- a/plugin/src/App/StatusPages/Settings/init.lua
+++ b/plugin/src/App/StatusPages/Settings/init.lua
@@ -156,7 +156,9 @@ function SettingsPage:render()
OpenScriptsExternally = e(Setting, {
id = "openScriptsExternally",
name = "Open Scripts Externally",
- description = "EXPERIMENTAL! Attempt to open scripts in an external editor",
+ description = "Attempt to open scripts in an external editor",
+ locked = self.props.syncActive,
+ experimental = true,
transparency = self.props.transparency,
layoutOrder = 5,
}),
@@ -164,7 +166,9 @@ function SettingsPage:render()
TwoWaySync = e(Setting, {
id = "twoWaySync",
name = "Two-Way Sync",
- description = "EXPERIMENTAL! Editing files in Studio will sync them into the filesystem",
+ description = "Editing files in Studio will sync them into the filesystem",
+ locked = self.props.syncActive,
+ experimental = true,
transparency = self.props.transparency,
layoutOrder = 6,
}),
diff --git a/plugin/src/App/Theme.lua b/plugin/src/App/Theme.lua
index 309045be..8ef05fab 100644
--- a/plugin/src/App/Theme.lua
+++ b/plugin/src/App/Theme.lua
@@ -100,6 +100,7 @@ local lightTheme = strict("LightTheme", {
Remove = hexColor(0xffbdba),
Edit = hexColor(0xbacdff),
Row = hexColor(0x000000),
+ Warning = hexColor(0xFF8E3C),
},
ConnectionDetails = {
ProjectNameColor = hexColor(0x00000),
@@ -195,6 +196,7 @@ local darkTheme = strict("DarkTheme", {
Remove = hexColor(0x3F2D32),
Edit = hexColor(0x193345),
Row = hexColor(0xFFFFFF),
+ Warning = hexColor(0xFF8E3C),
},
ConnectionDetails = {
ProjectNameColor = hexColor(0xFFFFFF),
diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua
index bb1ce6b3..9ffc8b8e 100644
--- a/plugin/src/App/init.lua
+++ b/plugin/src/App/init.lua
@@ -1,3 +1,4 @@
+local ChangeHistoryService = game:GetService("ChangeHistoryService")
local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local RunService = game:GetService("RunService")
@@ -19,6 +20,7 @@ local ServeSession = require(Plugin.ServeSession)
local ApiContext = require(Plugin.ApiContext)
local HeadlessAPI = require(Plugin.HeadlessAPI)
local PatchSet = require(Plugin.PatchSet)
+local PatchTree = require(Plugin.PatchTree)
local preloadAssets = require(Plugin.preloadAssets)
local soundPlayer = require(Plugin.soundPlayer)
local ignorePlaceIds = require(Plugin.ignorePlaceIds)
@@ -61,12 +63,71 @@ function App:init()
self.confirmationEvent = self.confirmationBindable.Event
self.notifId = 0
+ self.waypointConnection = ChangeHistoryService.OnUndo:Connect(function(action: string)
+ if not string.find(action, "^Rojo: Patch") then
+ return
+ end
+
+ local undoConnection, redoConnection = nil, nil
+ local function cleanup()
+ undoConnection:Disconnect()
+ redoConnection:Disconnect()
+ end
+
+ Log.warn(
+ string.format(
+ "You've undone '%s'.\nIf this was not intended, please Redo in the topbar or with Ctrl/⌘+Y.",
+ action
+ )
+ )
+ local dismissNotif = self:addNotification(
+ string.format("You've undone '%s'.\nIf this was not intended, please restore.", action),
+ 10,
+ {
+ Restore = {
+ text = "Restore",
+ style = "Solid",
+ layoutOrder = 1,
+ onClick = function(notification)
+ cleanup()
+ notification:dismiss()
+ ChangeHistoryService:Redo()
+ end,
+ },
+ Dismiss = {
+ text = "Dismiss",
+ style = "Bordered",
+ layoutOrder = 2,
+ onClick = function(notification)
+ cleanup()
+ notification:dismiss()
+ end,
+ },
+ }
+ )
+
+ undoConnection = ChangeHistoryService.OnUndo:Once(function()
+ -- Our notif is now out of date- redoing will not restore the patch
+ -- since we've undone even further. Dismiss the notif.
+ cleanup()
+ dismissNotif()
+ end)
+ redoConnection = ChangeHistoryService.OnRedo:Once(function(redoneAction: string)
+ if redoneAction == action then
+ -- The user has restored the patch, so we can dismiss the notif
+ cleanup()
+ dismissNotif()
+ end
+ end)
+ end)
+
self:setState({
appStatus = AppStatus.NotConnected,
guiEnabled = false,
confirmData = {},
patchData = {
patch = PatchSet.newEmpty(),
+ unapplied = PatchSet.newEmpty(),
timestamp = os.time(),
},
notifications = {},
@@ -132,7 +193,7 @@ function App:init()
onClick = function(notification)
notification:dismiss()
self:startSession()
- end
+ end,
},
Dismiss = {
text = "Dismiss",
@@ -146,7 +207,16 @@ function App:init()
end
end
-function App:addNotification(text: string, timeout: number?, actions: { [string]: {text: string, style: string, layoutOrder: number, onClick: (any) -> ()} }?)
+function App:willUnmount()
+ self.waypointConnection:Disconnect()
+ self.confirmationBindable:Destroy()
+end
+
+function App:addNotification(
+ text: string,
+ timeout: number?,
+ actions: { [string]: { text: string, style: string, layoutOrder: number, onClick: (any) -> () } }?
+)
if not Settings:get("showNotifications") then
return
end
@@ -199,6 +269,10 @@ function App:addThirdPartyNotification(source: string, text: string, timeout: nu
end
function App:closeNotification(id: number)
+ if not self.state.notifications[id] then
+ return
+ end
+
local notifications = table.clone(self.state.notifications)
notifications[id] = nil
@@ -209,26 +283,38 @@ end
function App:getPriorEndpoint()
local priorEndpoints = Settings:get("priorEndpoints")
- if not priorEndpoints then return end
+ if not priorEndpoints then
+ return
+ end
local id = tostring(game.PlaceId)
- if ignorePlaceIds[id] then return end
+ if ignorePlaceIds[id] then
+ return
+ end
local place = priorEndpoints[id]
- if not place then return end
+ if not place then
+ return
+ end
return place.host, place.port
end
function App:getLastSyncTimestamp()
local priorEndpoints = Settings:get("priorEndpoints")
- if not priorEndpoints then return end
+ if not priorEndpoints then
+ return
+ end
local id = tostring(game.PlaceId)
- if ignorePlaceIds[id] then return end
+ if ignorePlaceIds[id] then
+ return
+ end
local place = priorEndpoints[id]
- if not place then return end
+ if not place then
+ return
+ end
return place.timestamp
end
@@ -248,7 +334,9 @@ function App:setPriorEndpoint(host: string, port: string)
end
local id = tostring(game.PlaceId)
- if ignorePlaceIds[id] then return end
+ if ignorePlaceIds[id] then
+ return
+ end
priorEndpoints[id] = {
host = if host ~= Config.defaultHost then host else nil,
@@ -257,7 +345,6 @@ function App:setPriorEndpoint(host: string, port: string)
}
Log.trace("Saved last used endpoint for {}", game.PlaceId)
-
Settings:set("priorEndpoints", priorEndpoints)
end
@@ -393,35 +480,51 @@ function App:startSession(host: string?, port: string?)
twoWaySync = sessionOptions.twoWaySync,
})
- serveSession:onPatchApplied(function(patch, _unapplied)
+ self.cleanupPrecommit = serveSession.__reconciler:hookPrecommit(function(patch, instanceMap)
+ -- Build new tree for patch
+ self:setState({
+ patchTree = PatchTree.build(patch, instanceMap, { "Property", "Old", "New" }),
+ })
+ end)
+ self.cleanupPostcommit = serveSession.__reconciler:hookPostcommit(function(patch, instanceMap, unappliedPatch)
+ -- Update tree with unapplied metadata
+ self:setState(function(prevState)
+ return {
+ patchTree = PatchTree.updateMetadata(prevState.patchTree, patch, instanceMap, unappliedPatch),
+ }
+ end)
+ end)
+
+ serveSession:hookPostcommit(function(patch, _instanceMap, unapplied)
+ local now = os.time()
+ local old = self.state.patchData
+
if PatchSet.isEmpty(patch) then
- -- Ignore empty patches
+ -- Ignore empty patch, but update timestamp
+ self:setState({
+ patchData = {
+ patch = old.patch,
+ unapplied = old.unapplied,
+ timestamp = now,
+ },
+ })
return
end
- local now = os.time()
-
- local old = self.state.patchData
if now - old.timestamp < 2 then
-- Patches that apply in the same second are
-- considered to be part of the same change for human clarity
- local merged = PatchSet.newEmpty()
- PatchSet.assign(merged, old.patch, patch)
-
- self:setState({
- patchData = {
- patch = merged,
- timestamp = now,
- },
- })
- else
- self:setState({
- patchData = {
- patch = patch,
- timestamp = now,
- },
- })
+ patch = PatchSet.assign(PatchSet.newEmpty(), old.patch, patch)
+ unapplied = PatchSet.assign(PatchSet.newEmpty(), old.unapplied, unapplied)
end
+
+ self:setState({
+ patchData = {
+ patch = patch,
+ unapplied = unapplied,
+ timestamp = now,
+ },
+ })
end)
serveSession:onStatusChanged(function(status, details)
@@ -449,6 +552,13 @@ function App:startSession(host: string?, port: string?)
elseif status == ServeSession.Status.Disconnected then
self.serveSession = nil
self:releaseSyncLock()
+ self:setState({
+ patchData = {
+ patch = PatchSet.newEmpty(),
+ unapplied = PatchSet.newEmpty(),
+ timestamp = os.time(),
+ },
+ })
-- Details being present indicates that this
-- disconnection was from an error.
@@ -540,6 +650,13 @@ function App:endSession()
appStatus = AppStatus.NotConnected,
})
+ if self.cleanupPrecommit ~= nil then
+ self.cleanupPrecommit()
+ end
+ if self.cleanupPostcommit ~= nil then
+ self.cleanupPostcommit()
+ end
+
Log.trace("Session terminated by user")
end
@@ -602,8 +719,8 @@ function App:render()
initDockState = Enum.InitialDockState.Right,
initEnabled = false,
overridePreviousState = false,
- floatingSize = Vector2.new(300, 200),
- minimumSize = Vector2.new(300, 120),
+ floatingSize = Vector2.new(320, 210),
+ minimumSize = Vector2.new(300, 210),
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
@@ -632,6 +749,7 @@ function App:render()
end,
onNavigateSettings = function()
+ self.backPage = AppStatus.NotConnected
self:setState({
appStatus = AppStatus.Settings,
})
@@ -658,18 +776,28 @@ function App:render()
Connected = createPageElement(AppStatus.Connected, {
projectName = self.state.projectName,
address = self.state.address,
+ patchTree = self.state.patchTree,
patchData = self.state.patchData,
serveSession = self.serveSession,
onDisconnect = function()
self:endSession()
end,
+
+ onNavigateSettings = function()
+ self.backPage = AppStatus.Connected
+ self:setState({
+ appStatus = AppStatus.Settings,
+ })
+ end,
}),
Settings = createPageElement(AppStatus.Settings, {
+ syncActive = self.serveSession ~= nil and self.serveSession:getStatus() == ServeSession.Status.Connected,
+
onBack = function()
self:setState({
- appStatus = AppStatus.NotConnected,
+ appStatus = self.backPage or AppStatus.NotConnected,
})
end,
diff --git a/plugin/src/Assets.lua b/plugin/src/Assets.lua
index d99e32ed..9abbe41f 100644
--- a/plugin/src/Assets.lua
+++ b/plugin/src/Assets.lua
@@ -36,9 +36,11 @@ local Assets = {
Checkbox = {
Active = "rbxassetid://6016251644",
Inactive = "rbxassetid://6016251963",
+ Locked = "rbxassetid://14011257320",
},
Dropdown = {
Arrow = "rbxassetid://10131770538",
+ Locked = "rbxassetid://14011257320",
},
Spinner = {
Foreground = "rbxassetid://3222731032",
diff --git a/plugin/src/PatchSet.lua b/plugin/src/PatchSet.lua
index c9f621dc..a9bebd80 100644
--- a/plugin/src/PatchSet.lua
+++ b/plugin/src/PatchSet.lua
@@ -212,13 +212,24 @@ function PatchSet.countChanges(patch)
local count = 0
for _ in patch.added do
+ -- Adding an instance is 1 change
count += 1
end
for _ in patch.removed do
+ -- Removing an instance is 1 change
count += 1
end
- for _ in patch.updated do
- count += 1
+ for _, update in patch.updated do
+ -- Updating an instance is 1 change per property updated
+ for _ in update.changedProperties do
+ count += 1
+ end
+ if update.changedName ~= nil then
+ count += 1
+ end
+ if update.changedClassName ~= nil then
+ count += 1
+ end
end
return count
diff --git a/plugin/src/PatchTree.lua b/plugin/src/PatchTree.lua
new file mode 100644
index 00000000..b241472d
--- /dev/null
+++ b/plugin/src/PatchTree.lua
@@ -0,0 +1,434 @@
+--[[
+ Methods to turn PatchSets into trees matching the DataModel containing
+ the changes and metadata for use in the PatchVisualizer component.
+]]
+
+local HttpService = game:GetService("HttpService")
+
+local Rojo = script:FindFirstAncestor("Rojo")
+local Plugin = Rojo.Plugin
+local Packages = Rojo.Packages
+
+local Log = require(Packages.Log)
+
+local Types = require(Plugin.Types)
+local decodeValue = require(Plugin.Reconciler.decodeValue)
+local getProperty = require(Plugin.Reconciler.getProperty)
+
+local function alphabeticalNext(t, state)
+ -- Equivalent of the next function, but returns the keys in the alphabetic
+ -- order of node names. We use a temporary ordered key table that is stored in the
+ -- table being iterated.
+
+ local key = nil
+ if state == nil then
+ -- First iteration, generate the index
+ local orderedIndex, i = table.create(5), 0
+ for k in t do
+ i += 1
+ orderedIndex[i] = k
+ end
+ table.sort(orderedIndex, function(a, b)
+ local nodeA, nodeB = t[a], t[b]
+ return (nodeA.name or "") < (nodeB.name or "")
+ end)
+
+ t.__orderedIndex = orderedIndex
+ key = orderedIndex[1]
+ else
+ -- Fetch the next value
+ for i, orderedState in t.__orderedIndex do
+ if orderedState == state then
+ key = t.__orderedIndex[i + 1]
+ break
+ end
+ end
+ end
+
+ if key then
+ return key, t[key]
+ end
+
+ -- No more value to return, cleanup
+ t.__orderedIndex = nil
+
+ return
+end
+
+local function alphabeticalPairs(t)
+ -- Equivalent of the pairs() iterator, but sorted
+ return alphabeticalNext, t, nil
+end
+
+local Tree = {}
+Tree.__index = Tree
+
+function Tree.new()
+ local tree = {
+ idToNode = {},
+ ROOT = {
+ className = "DataModel",
+ name = "ROOT",
+ children = {},
+ },
+ }
+ -- Add ROOT to idToNode or it won't be found by getNode since that searches *within* ROOT
+ tree.idToNode["ROOT"] = tree.ROOT
+
+ return setmetatable(tree, Tree)
+end
+
+-- Iterates over all sub-nodes, depth first
+-- node is where to start from, defaults to root
+-- depth is used for recursion but can be used to set the starting depth
+function Tree:forEach(callback, node, depth)
+ depth = depth or 1
+ for _, child in alphabeticalPairs(if node then node.children else self.ROOT.children) do
+ callback(child, depth)
+ if type(child.children) == "table" then
+ self:forEach(callback, child, depth + 1)
+ end
+ end
+end
+
+-- Finds a node by id, depth first
+-- searchNode is the node to start the search within, defaults to root
+function Tree:getNode(id, searchNode)
+ if self.idToNode[id] then
+ return self.idToNode[id]
+ end
+
+ local searchChildren = (searchNode or self.ROOT).children
+ for nodeId, node in searchChildren do
+ if nodeId == id then
+ self.idToNode[id] = node
+ return node
+ end
+ local descendant = self:getNode(id, node)
+ if descendant then
+ return descendant
+ end
+ end
+
+ return nil
+end
+
+-- Adds a node to the tree as a child of the node with id == parent
+-- If parent is nil, it defaults to root
+-- props must contain id, and cannot contain children or parentId
+-- other than those three, it can hold anything
+function Tree:addNode(parent, props)
+ assert(props.id, "props must contain id")
+
+ parent = parent or "ROOT"
+
+ local node = self:getNode(props.id)
+ if node then
+ -- Update existing node
+ for k, v in props do
+ node[k] = v
+ end
+ return node
+ end
+
+ node = table.clone(props)
+ node.children = {}
+ node.parentId = parent
+
+ local parentNode = self:getNode(parent)
+ if not parentNode then
+ Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
+ return
+ end
+
+ parentNode.children[node.id] = node
+ self.idToNode[node.id] = node
+
+ return node
+end
+
+-- Given a list of ancestor ids in descending order, builds the nodes for them
+-- using the patch and instanceMap info
+function Tree:buildAncestryNodes(ancestryIds: { string }, patch, instanceMap)
+ -- Build nodes for ancestry by going up the tree
+ local previousId = "ROOT"
+ for _, ancestorId in ancestryIds do
+ local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
+ if not value then
+ Log.warn("Failed to find ancestor object for " .. ancestorId)
+ continue
+ end
+ self:addNode(previousId, {
+ id = ancestorId,
+ className = value.ClassName,
+ name = value.Name,
+ instance = if typeof(value) == "Instance" then value else nil,
+ })
+ previousId = ancestorId
+ end
+end
+
+local PatchTree = {}
+
+-- Builds a new tree from a patch and instanceMap
+-- uses changeListHeaders in node.changeList
+function PatchTree.build(patch, instanceMap, changeListHeaders)
+ local tree = Tree.new()
+
+ for _, change in patch.updated do
+ local instance = instanceMap.fromIds[change.id]
+ if not instance then
+ continue
+ end
+
+ -- Gather ancestors from existing DOM
+ local ancestryIds = {}
+ local parentObject = instance.Parent
+ local parentId = instanceMap.fromInstances[parentObject]
+ while parentObject do
+ table.insert(ancestryIds, 1, parentId)
+ parentObject = parentObject.Parent
+ parentId = instanceMap.fromInstances[parentObject]
+ end
+
+ tree:buildAncestryNodes(ancestryIds, patch, instanceMap)
+
+ -- Gather detail text
+ local changeList, hint = nil, nil
+ if next(change.changedProperties) or change.changedName then
+ changeList = {}
+
+ local hintBuffer, i = {}, 0
+ local function addProp(prop: string, current: any?, incoming: any?, metadata: any?)
+ i += 1
+ hintBuffer[i] = prop
+ changeList[i] = { prop, current, incoming, metadata }
+ end
+
+ -- Gather the changes
+
+ if change.changedName then
+ addProp("Name", instance.Name, change.changedName)
+ end
+
+ for prop, incoming in change.changedProperties do
+ local incomingSuccess, incomingValue = decodeValue(incoming, instanceMap)
+ local currentSuccess, currentValue = getProperty(instance, prop)
+
+ addProp(
+ prop,
+ if currentSuccess then currentValue else "[Error]",
+ if incomingSuccess then incomingValue else next(incoming)
+ )
+ end
+
+ -- Finalize detail values
+
+ -- Trim hint to top 3
+ table.sort(hintBuffer)
+ if #hintBuffer > 3 then
+ hintBuffer = {
+ hintBuffer[1],
+ hintBuffer[2],
+ hintBuffer[3],
+ i - 3 .. " more",
+ }
+ end
+ hint = table.concat(hintBuffer, ", ")
+
+ -- Sort changes and add header
+ table.sort(changeList, function(a, b)
+ return a[1] < b[1]
+ end)
+ table.insert(changeList, 1, changeListHeaders)
+ end
+
+ -- Add this node to tree
+ tree:addNode(instanceMap.fromInstances[instance.Parent], {
+ id = change.id,
+ patchType = "Edit",
+ className = instance.ClassName,
+ name = instance.Name,
+ instance = instance,
+ hint = hint,
+ changeList = changeList,
+ })
+ end
+
+ for _, idOrInstance in patch.removed do
+ local instance = if Types.RbxId(idOrInstance) then instanceMap.fromIds[idOrInstance] else idOrInstance
+ if not instance then
+ -- If we're viewing a past patch, the instance is already removed
+ -- and we therefore cannot get the tree for it anymore
+ continue
+ end
+
+ -- Gather ancestors from existing DOM
+ -- (note that they may have no ID if they're being removed as unknown)
+ local ancestryIds = {}
+ local parentObject = instance.Parent
+ local parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
+ while parentObject do
+ instanceMap:insert(parentId, parentObject) -- This ensures we can find the parent later
+ table.insert(ancestryIds, 1, parentId)
+ parentObject = parentObject.Parent
+ parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
+ end
+
+ tree:buildAncestryNodes(ancestryIds, patch, instanceMap)
+
+ -- Add this node to tree
+ local nodeId = instanceMap.fromInstances[instance] or HttpService:GenerateGUID(false)
+ instanceMap:insert(nodeId, instance)
+ tree:addNode(instanceMap.fromInstances[instance.Parent], {
+ id = nodeId,
+ patchType = "Remove",
+ className = instance.ClassName,
+ name = instance.Name,
+ instance = instance,
+ })
+ end
+
+ for id, change in patch.added do
+ -- Gather ancestors from existing DOM or future additions
+ local ancestryIds = {}
+ local parentId = change.Parent
+ local parentData = patch.added[parentId]
+ local parentObject = instanceMap.fromIds[parentId]
+ while parentId do
+ table.insert(ancestryIds, 1, parentId)
+ parentId = nil
+
+ if parentData then
+ -- object is parented to an instance that does not exist yet
+ parentId = parentData.Parent
+ parentData = patch.added[parentId]
+ parentObject = instanceMap.fromIds[parentId]
+ elseif parentObject then
+ -- object is parented to an instance that exists
+ parentObject = parentObject.Parent
+ parentId = instanceMap.fromInstances[parentObject]
+ parentData = patch.added[parentId]
+ end
+ end
+
+ tree:buildAncestryNodes(ancestryIds, patch, instanceMap)
+
+ -- Gather detail text
+ local changeList, hint = nil, nil
+ if next(change.Properties) then
+ changeList = {}
+
+ local hintBuffer, i = {}, 0
+ for prop, incoming in change.Properties do
+ i += 1
+ hintBuffer[i] = prop
+
+ local success, incomingValue = decodeValue(incoming, instanceMap)
+ if success then
+ table.insert(changeList, { prop, "N/A", incomingValue })
+ else
+ table.insert(changeList, { prop, "N/A", next(incoming) })
+ end
+ end
+
+ -- Finalize detail values
+
+ -- Trim hint to top 3
+ table.sort(hintBuffer)
+ if #hintBuffer > 3 then
+ hintBuffer = {
+ hintBuffer[1],
+ hintBuffer[2],
+ hintBuffer[3],
+ i - 3 .. " more",
+ }
+ end
+ hint = table.concat(hintBuffer, ", ")
+
+ -- Sort changes and add header
+ table.sort(changeList, function(a, b)
+ return a[1] < b[1]
+ end)
+ table.insert(changeList, 1, changeListHeaders)
+ end
+
+ -- Add this node to tree
+ tree:addNode(change.Parent, {
+ id = change.Id,
+ patchType = "Add",
+ className = change.ClassName,
+ name = change.Name,
+ hint = hint,
+ changeList = changeList,
+ instance = instanceMap.fromIds[id],
+ })
+ end
+
+ return tree
+end
+
+-- Creates a deep copy of a tree for immutability purposes in Roact
+function PatchTree.clone(tree)
+ if not tree then return end
+
+ local newTree = Tree.new()
+ tree:forEach(function(node)
+ newTree:addNode(node.parentId, table.clone(node))
+ end)
+
+ return newTree
+end
+
+-- Updates the metadata of a tree with the unapplied patch and currently existing instances
+-- Builds a new tree from the data if one isn't provided
+-- Always returns a new tree for immutability purposes in Roact
+function PatchTree.updateMetadata(tree, patch, instanceMap, unappliedPatch)
+ if tree then
+ tree = PatchTree.clone(tree)
+ else
+ tree = PatchTree.build(patch, instanceMap)
+ end
+
+ -- Update isWarning metadata
+ for _, failedChange in unappliedPatch.updated do
+ local node = tree:getNode(failedChange.id)
+ if node then
+ node.isWarning = true
+ Log.trace("Marked node as warning: {} {}", node.id, node.name)
+
+ if node.changeList then
+ for _, change in node.changeList do
+ if failedChange.changedProperties[change[1]] then
+ Log.trace(" Marked property as warning: {}", change[1])
+ if change[4] == nil then
+ change[4] = {}
+ end
+ change[4].isWarning = true
+ end
+ end
+ end
+ end
+ end
+
+ -- Update if instances exist
+ tree:forEach(function(node)
+ if node.instance then
+ if node.instance.Parent == nil and node.instance ~= game then
+ -- This instance has been removed
+ Log.trace("Removed instance from node: {} {}", node.id, node.name)
+ node.instance = nil
+ end
+ else
+ -- This instance may have been added
+ node.instance = instanceMap.fromIds[node.id]
+ if node.instance then
+ Log.trace("Added instance to node: {} {}", node.id, node.name)
+ end
+ end
+ end)
+
+ return tree
+end
+
+return PatchTree
diff --git a/plugin/src/Reconciler/applyPatch.lua b/plugin/src/Reconciler/applyPatch.lua
index a625ebe9..4faa7310 100644
--- a/plugin/src/Reconciler/applyPatch.lua
+++ b/plugin/src/Reconciler/applyPatch.lua
@@ -5,6 +5,8 @@
Patches can come from the server or be generated by the client.
]]
+local ChangeHistoryService = game:GetService("ChangeHistoryService")
+
local Packages = script.Parent.Parent.Parent.Packages
local Log = require(Packages.Log)
@@ -17,6 +19,8 @@ local reify = require(script.Parent.reify)
local setProperty = require(script.Parent.setProperty)
local function applyPatch(instanceMap, patch)
+ local patchTimestamp = DateTime.now():FormatLocalTime("LTS", "en-us")
+
-- Tracks any portions of the patch that could not be applied to the DOM.
local unappliedPatch = PatchSet.newEmpty()
@@ -199,6 +203,8 @@ local function applyPatch(instanceMap, patch)
end
end
+ ChangeHistoryService:SetWaypoint("Rojo: Patch " .. patchTimestamp)
+
return unappliedPatch
end
diff --git a/plugin/src/Reconciler/init.lua b/plugin/src/Reconciler/init.lua
index 1db295a4..12945782 100644
--- a/plugin/src/Reconciler/init.lua
+++ b/plugin/src/Reconciler/init.lua
@@ -3,6 +3,9 @@
and mutating the Roblox DOM.
]]
+local Packages = script.Parent.Parent.Packages
+local Log = require(Packages.Log)
+
local applyPatch = require(script.applyPatch)
local hydrate = require(script.hydrate)
local diff = require(script.diff)
@@ -14,13 +17,63 @@ function Reconciler.new(instanceMap)
local self = {
-- Tracks all of the instances known by the reconciler by ID.
__instanceMap = instanceMap,
+ __precommitCallbacks = {},
+ __postcommitCallbacks = {},
}
return setmetatable(self, Reconciler)
end
+function Reconciler:hookPrecommit(callback: (patch: any, instanceMap: any) -> ()): () -> ()
+ table.insert(self.__precommitCallbacks, callback)
+ Log.trace("Added precommit callback: {}", callback)
+
+ return function()
+ -- Remove the callback from the list
+ for i, cb in self.__precommitCallbacks do
+ if cb == callback then
+ table.remove(self.__precommitCallbacks, i)
+ Log.trace("Removed precommit callback: {}", callback)
+ break
+ end
+ end
+ end
+end
+
+function Reconciler:hookPostcommit(callback: (patch: any, instanceMap: any, unappliedPatch: any) -> ()): () -> ()
+ table.insert(self.__postcommitCallbacks, callback)
+ Log.trace("Added postcommit callback: {}", callback)
+
+ return function()
+ -- Remove the callback from the list
+ for i, cb in self.__postcommitCallbacks do
+ if cb == callback then
+ table.remove(self.__postcommitCallbacks, i)
+ Log.trace("Removed postcommit callback: {}", callback)
+ break
+ end
+ end
+ end
+end
+
function Reconciler:applyPatch(patch)
- return applyPatch(self.__instanceMap, patch)
+ for _, callback in self.__precommitCallbacks do
+ local success, err = pcall(callback, patch, self.__instanceMap)
+ if not success then
+ Log.warn("Precommit hook errored: {}", err)
+ end
+ end
+
+ local unappliedPatch = applyPatch(self.__instanceMap, patch)
+
+ for _, callback in self.__postcommitCallbacks do
+ local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch)
+ if not success then
+ Log.warn("Postcommit hook errored: {}", err)
+ end
+ end
+
+ return unappliedPatch
end
function Reconciler:hydrate(virtualInstances, rootId, rootInstance)
@@ -31,4 +84,4 @@ function Reconciler:diff(virtualInstances, rootId)
return diff(self.__instanceMap, virtualInstances, rootId)
end
-return Reconciler
\ No newline at end of file
+return Reconciler
diff --git a/plugin/src/ServeSession.lua b/plugin/src/ServeSession.lua
index 08dc4041..6a3fcdc4 100644
--- a/plugin/src/ServeSession.lua
+++ b/plugin/src/ServeSession.lua
@@ -97,7 +97,6 @@ function ServeSession.new(options)
__instanceMap = instanceMap,
__changeBatcher = changeBatcher,
__statusChangedCallback = nil,
- __patchAppliedCallback = nil,
__connections = connections,
}
@@ -129,8 +128,12 @@ function ServeSession:setConfirmCallback(callback)
self.__userConfirmCallback = callback
end
-function ServeSession:onPatchApplied(callback)
- self.__patchAppliedCallback = callback
+function ServeSession:hookPrecommit(callback)
+ return self.__reconciler:hookPrecommit(callback)
+end
+
+function ServeSession:hookPostcommit(callback)
+ return self.__reconciler:hookPostcommit(callback)
end
function ServeSession:start()
@@ -291,35 +294,45 @@ function ServeSession:__initialSync(serverInfo)
Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
end
- if self.__patchAppliedCallback then
- pcall(self.__patchAppliedCallback, catchUpPatch, unappliedPatch)
- end
end
end)
end
function ServeSession:__mainSyncLoop()
- return self.__apiContext:retrieveMessages()
- :andThen(function(messages)
- Log.trace("Serve session {} retrieved {} messages", tostring(self), #messages)
+ return Promise.new(function(resolve, reject)
+ while self.__status == Status.Connected do
+ local success, result = self.__apiContext:retrieveMessages()
+ :andThen(function(messages)
+ if self.__status == Status.Disconnected then
+ -- In the time it took to retrieve messages, we disconnected
+ -- so we just resolve immediately without patching anything
+ return
+ end
- for _, message in ipairs(messages) do
- local unappliedPatch = self.__reconciler:applyPatch(message)
+ Log.trace("Serve session {} retrieved {} messages", tostring(self), #messages)
- if not PatchSet.isEmpty(unappliedPatch) then
- Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
- PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
- end
+ for _, message in messages do
+ local unappliedPatch = self.__reconciler:applyPatch(message)
- if self.__patchAppliedCallback then
- pcall(self.__patchAppliedCallback, message, unappliedPatch)
- end
+ if not PatchSet.isEmpty(unappliedPatch) then
+ Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
+ PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
+ end
+ end
+ end):await()
+
+ if self.__status == Status.Disconnected then
+ -- If we are no longer connected after applying, we stop silently
+ -- without checking for errors as they are no longer relevant
+ break
+ elseif success == false then
+ reject(result)
end
+ end
- if self.__status ~= Status.Disconnected then
- return self:__mainSyncLoop()
- end
- end)
+ -- We are no longer connected, so we resolve the promise
+ resolve()
+ end)
end
function ServeSession:__stopInternal(err)
diff --git a/src/snapshot/instance_snapshot.rs b/src/snapshot/instance_snapshot.rs
index 36c51821..0b3a4fca 100644
--- a/src/snapshot/instance_snapshot.rs
+++ b/src/snapshot/instance_snapshot.rs
@@ -17,9 +17,8 @@ use super::InstanceMetadata;
// - Replace use of Variant with a sum of Variant + borrowed value
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InstanceSnapshot {
- // FIXME: Don't use Option[ anymore!
/// A temporary ID applied to the snapshot that's used for Ref properties.
- pub snapshot_id: Option][,
+ pub snapshot_id: Ref,
/// Rojo-specific metadata associated with the instance.
pub metadata: InstanceMetadata,
@@ -42,7 +41,7 @@ pub struct InstanceSnapshot {
impl InstanceSnapshot {
pub fn new() -> Self {
Self {
- snapshot_id: None,
+ snapshot_id: Ref::none(),
metadata: InstanceMetadata::default(),
name: Cow::Borrowed("DEFAULT"),
class_name: Cow::Borrowed("DEFAULT"),
@@ -88,7 +87,7 @@ impl InstanceSnapshot {
}
}
- pub fn snapshot_id(self, snapshot_id: Option][) -> Self {
+ pub fn snapshot_id(self, snapshot_id: Ref) -> Self {
Self {
snapshot_id,
..self
@@ -120,7 +119,7 @@ impl InstanceSnapshot {
.collect();
Self {
- snapshot_id: Some(id),
+ snapshot_id: id,
metadata: InstanceMetadata::default(),
name: Cow::Owned(instance.name),
class_name: Cow::Owned(instance.class),
diff --git a/src/snapshot/patch_apply.rs b/src/snapshot/patch_apply.rs
index 5818776c..572b8455 100644
--- a/src/snapshot/patch_apply.rs
+++ b/src/snapshot/patch_apply.rs
@@ -135,7 +135,7 @@ fn apply_add_child(
context.has_refs_to_rewrite.insert(id);
}
- if let Some(snapshot_id) = snapshot_id {
+ if snapshot_id.is_some() {
context.snapshot_id_to_instance_id.insert(snapshot_id, id);
}
@@ -231,7 +231,7 @@ mod test {
let root_id = tree.get_root_id();
let snapshot = InstanceSnapshot {
- snapshot_id: None,
+ snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("Foo"),
class_name: Cow::Borrowed("Bar"),
diff --git a/src/snapshot/patch_compute.rs b/src/snapshot/patch_compute.rs
index f5d5a67b..5d7abc69 100644
--- a/src/snapshot/patch_compute.rs
+++ b/src/snapshot/patch_compute.rs
@@ -77,8 +77,10 @@ fn compute_patch_set_internal(
id: Ref,
patch_set: &mut PatchSet,
) {
- if let Some(snapshot_id) = snapshot.snapshot_id {
- context.snapshot_id_to_instance_id.insert(snapshot_id, id);
+ if snapshot.snapshot_id.is_some() {
+ context
+ .snapshot_id_to_instance_id
+ .insert(snapshot.snapshot_id, id);
}
let instance = tree
@@ -244,7 +246,7 @@ mod test {
// addition of a prop named Self, which is a self-referential Ref.
let snapshot_id = Ref::new();
let snapshot = InstanceSnapshot {
- snapshot_id: Some(snapshot_id),
+ snapshot_id: snapshot_id,
properties: hashmap! {
"Self".to_owned() => Variant::Ref(snapshot_id),
},
@@ -286,13 +288,13 @@ mod test {
// This patch describes the existing instance with a new child added.
let snapshot_id = Ref::new();
let snapshot = InstanceSnapshot {
- snapshot_id: Some(snapshot_id),
+ snapshot_id: snapshot_id,
children: vec![InstanceSnapshot {
properties: hashmap! {
"Self".to_owned() => Variant::Ref(snapshot_id),
},
- snapshot_id: None,
+ snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("child"),
class_name: Cow::Borrowed("child"),
@@ -311,7 +313,7 @@ mod test {
added_instances: vec![PatchAdd {
parent_id: root_id,
instance: InstanceSnapshot {
- snapshot_id: None,
+ snapshot_id: Ref::none(),
metadata: Default::default(),
properties: hashmap! {
"Self".to_owned() => Variant::Ref(root_id),
diff --git a/src/snapshot/tests/compute.rs b/src/snapshot/tests/compute.rs
index 309d73b6..fc6a0b63 100644
--- a/src/snapshot/tests/compute.rs
+++ b/src/snapshot/tests/compute.rs
@@ -3,6 +3,7 @@ use std::borrow::Cow;
use insta::assert_yaml_snapshot;
use maplit::hashmap;
+use rbx_dom_weak::types::Ref;
use rojo_insta_ext::RedactionMap;
use crate::snapshot::{compute_patch_set, InstanceSnapshot, RojoTree};
@@ -15,7 +16,7 @@ fn set_name_and_class_name() {
redactions.intern(tree.get_root_id());
let snapshot = InstanceSnapshot {
- snapshot_id: None,
+ snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("Some Folder"),
class_name: Cow::Borrowed("Folder"),
@@ -37,7 +38,7 @@ fn set_property() {
redactions.intern(tree.get_root_id());
let snapshot = InstanceSnapshot {
- snapshot_id: None,
+ snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("ROOT"),
class_name: Cow::Borrowed("ROOT"),
@@ -70,7 +71,7 @@ fn remove_property() {
}
let snapshot = InstanceSnapshot {
- snapshot_id: None,
+ snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("ROOT"),
class_name: Cow::Borrowed("ROOT"),
@@ -92,13 +93,13 @@ fn add_child() {
redactions.intern(tree.get_root_id());
let snapshot = InstanceSnapshot {
- snapshot_id: None,
+ snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("ROOT"),
class_name: Cow::Borrowed("ROOT"),
properties: Default::default(),
children: vec![InstanceSnapshot {
- snapshot_id: None,
+ snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("New"),
class_name: Cow::Borrowed("Folder"),
@@ -131,7 +132,7 @@ fn remove_child() {
}
let snapshot = InstanceSnapshot {
- snapshot_id: None,
+ snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Borrowed("ROOT"),
class_name: Cow::Borrowed("ROOT"),
diff --git a/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap b/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap
index 97bd639e..c4695b7c 100644
--- a/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap
+++ b/src/snapshot/tests/snapshots/librojo__snapshot__tests__compute__add_child.snap
@@ -6,7 +6,7 @@ removed_instances: []
added_instances:
- parent_id: id-1
instance:
- snapshot_id: ~
+ snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
relevant_paths: []
@@ -16,3 +16,4 @@ added_instances:
properties: {}
children: []
updated_instances: []
+
diff --git a/src/snapshot_middleware/json_model.rs b/src/snapshot_middleware/json_model.rs
index 0f1388e4..b38c47a7 100644
--- a/src/snapshot_middleware/json_model.rs
+++ b/src/snapshot_middleware/json_model.rs
@@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::HashMap, path::Path, str};
use anyhow::Context;
use memofs::Vfs;
-use rbx_dom_weak::types::Attributes;
+use rbx_dom_weak::types::{Attributes, Ref};
use serde::Deserialize;
use crate::{
@@ -112,7 +112,7 @@ impl JsonModel {
}
Ok(InstanceSnapshot {
- snapshot_id: None,
+ snapshot_id: Ref::none(),
metadata: Default::default(),
name: Cow::Owned(name),
class_name: Cow::Owned(class_name),
diff --git a/src/snapshot_middleware/mod.rs b/src/snapshot_middleware/mod.rs
index 3d5de76b..c690a223 100644
--- a/src/snapshot_middleware/mod.rs
+++ b/src/snapshot_middleware/mod.rs
@@ -14,6 +14,7 @@ mod meta_file;
mod project;
mod rbxm;
mod rbxmx;
+mod toml;
mod txt;
mod util;
@@ -32,6 +33,7 @@ use self::{
project::snapshot_project,
rbxm::snapshot_rbxm,
rbxmx::snapshot_rbxmx,
+ toml::snapshot_toml,
txt::snapshot_txt,
util::PathExt,
};
@@ -117,6 +119,8 @@ pub fn snapshot_from_vfs(
return Ok(None);
} else if path.file_name_ends_with(".json") {
return snapshot_json(context, vfs, path);
+ } else if path.file_name_ends_with(".toml") {
+ return snapshot_toml(context, vfs, path);
} else if let Ok(name) = csv_name {
match name {
// init csv are handled elsewhere and should not turn into
diff --git a/src/snapshot_middleware/project.rs b/src/snapshot_middleware/project.rs
index f8fa7920..2d54a179 100644
--- a/src/snapshot_middleware/project.rs
+++ b/src/snapshot_middleware/project.rs
@@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::HashMap, path::Path};
use anyhow::{bail, Context};
use memofs::Vfs;
-use rbx_dom_weak::types::Attributes;
+use rbx_dom_weak::types::{Attributes, Ref};
use rbx_reflection::ClassTag;
use crate::{
@@ -271,7 +271,7 @@ pub fn snapshot_project_node(
));
Ok(Some(InstanceSnapshot {
- snapshot_id: None,
+ snapshot_id: Ref::none(),
name,
class_name,
properties,
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap
index 7a685efb..11a8d81c 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_from_vfs.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/csv.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap
index 70bae90d..eda82606 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__csv__test__csv_with_meta.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/csv.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: true
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap
index 948dda0b..e59da8db 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__empty_folder.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/dir.rs
-assertion_line: 127
expression: instance_snapshot
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap
index cf07b0ed..8c2ee9cc 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__dir__test__folder_in_folder.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/dir.rs
-assertion_line: 148
expression: instance_snapshot
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
@@ -23,7 +22,7 @@ name: foo
class_name: Folder
properties: {}
children:
- - snapshot_id: ~
+ - snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap
index 75329b85..b1cca7cb 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json__test__instance_from_vfs.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/json.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap
index a6a0743e..5d4db1fc 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/json_model.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
@@ -17,7 +16,7 @@ properties:
Value:
Int64: 5
children:
- - snapshot_id: ~
+ - snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
relevant_paths: []
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap
index a58fdb0f..5d4db1fc 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__json_model__test__model_from_vfs_legacy.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/json_model.rs
-assertion_line: 186
expression: instance_snapshot
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
@@ -17,7 +16,7 @@ properties:
Value:
Int64: 5
children:
- - snapshot_id: ~
+ - snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
relevant_paths: []
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__client_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__client_from_vfs.snap
index f774f561..40b4bbe2 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__client_from_vfs.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__client_from_vfs.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/lua.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_from_vfs.snap
index ac9ac72a..6877e9a9 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_from_vfs.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_from_vfs.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/lua.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_with_meta.snap
index f623955f..8dcc57a9 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_with_meta.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__module_with_meta.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/lua.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: true
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__script_disabled.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__script_disabled.snap
index 8a9b4776..3f040877 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__script_disabled.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__script_disabled.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/lua.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__script_with_meta.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__script_with_meta.snap
index 1234b48a..6bdd2a38 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__script_with_meta.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__script_with_meta.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/lua.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: true
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__server_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__server_from_vfs.snap
index 76473c75..63d33a0a 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__server_from_vfs.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__lua__test__server_from_vfs.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/lua.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap
index 42393420..795b14c1 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_from_direct_file.snap
@@ -2,7 +2,7 @@
source: src/snapshot_middleware/project.rs
expression: instance_snapshot
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: true
instigating_source:
@@ -14,3 +14,4 @@ name: direct-project
class_name: Model
properties: {}
children: []
+
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap
index 49b3b506..7427c9dc 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_path_property_overrides.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/project.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: true
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap
index ccf6b535..b50eb1be 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_children.snap
@@ -2,7 +2,7 @@
source: src/snapshot_middleware/project.rs
expression: instance_snapshot
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: true
instigating_source:
@@ -14,7 +14,7 @@ name: children
class_name: Folder
properties: {}
children:
- - snapshot_id: ~
+ - snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: true
instigating_source:
@@ -29,3 +29,4 @@ children:
class_name: Model
properties: {}
children: []
+
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap
index a520b7c7..d64a46e7 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project.snap
@@ -2,7 +2,7 @@
source: src/snapshot_middleware/project.rs
expression: instance_snapshot
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: true
instigating_source:
@@ -15,3 +15,4 @@ name: path-project
class_name: Model
properties: {}
children: []
+
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap
index c71b4d2d..0ceb043a 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_project_with_children.snap
@@ -2,7 +2,7 @@
source: src/snapshot_middleware/project.rs
expression: instance_snapshot
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: true
instigating_source:
@@ -15,7 +15,7 @@ name: path-child-project
class_name: Folder
properties: {}
children:
- - snapshot_id: ~
+ - snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: true
instigating_source:
@@ -30,3 +30,4 @@ children:
class_name: Model
properties: {}
children: []
+
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap
index 38b797c3..51a222f9 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_path_to_txt.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/project.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap
index 29a23a07..9b8e91ef 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_resolved_properties.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/project.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: true
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap
index a422e9f8..a07300bb 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__project__test__project_with_unresolved_properties.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/project.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: true
instigating_source:
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap
new file mode 100644
index 00000000..3529bf89
--- /dev/null
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__toml__test__instance_from_vfs.snap
@@ -0,0 +1,20 @@
+---
+source: src/snapshot_middleware/toml.rs
+expression: instance_snapshot
+---
+snapshot_id: "00000000000000000000000000000000"
+metadata:
+ ignore_unknown_instances: false
+ instigating_source:
+ Path: /foo.toml
+ relevant_paths:
+ - /foo.toml
+ - /foo.meta.json
+ context: {}
+name: foo
+class_name: ModuleScript
+properties:
+ Source:
+ String: "return {\n\t[\"1invalidident\"] = \"nice\",\n\tarray = {1, 2, 3},\n\tdates = {\n\t\tlocaldate = \"1979-05-27\",\n\t\tlocaldatetime = \"1979-05-27T07:32:00\",\n\t\tlocaltime = \"00:32:00.999999\",\n\t\toffset1 = \"1979-05-27T00:32:00.999999-07:00\",\n\t\toffset2 = \"1979-05-27T07:32:00Z\",\n\t},\n\t[\"false\"] = false,\n\tfloat = 1234.5452,\n\tint = 1234,\n\tobject = {\n\t\thello = \"world\",\n\t},\n\t[\"true\"] = true,\n}"
+children: []
+
diff --git a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap
index ad469490..b8a8d4af 100644
--- a/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap
+++ b/src/snapshot_middleware/snapshots/librojo__snapshot_middleware__txt__test__instance_from_vfs.snap
@@ -1,9 +1,8 @@
---
source: src/snapshot_middleware/txt.rs
expression: instance_snapshot
-
---
-snapshot_id: ~
+snapshot_id: "00000000000000000000000000000000"
metadata:
ignore_unknown_instances: false
instigating_source:
diff --git a/src/snapshot_middleware/toml.rs b/src/snapshot_middleware/toml.rs
new file mode 100644
index 00000000..6ee9be47
--- /dev/null
+++ b/src/snapshot_middleware/toml.rs
@@ -0,0 +1,123 @@
+use std::path::Path;
+
+use anyhow::Context;
+use maplit::hashmap;
+use memofs::{IoResultExt, Vfs};
+
+use crate::{
+ lua_ast::{Expression, Statement},
+ snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
+};
+
+use super::{meta_file::AdjacentMetadata, util::PathExt};
+
+pub fn snapshot_toml(
+ context: &InstanceContext,
+ vfs: &Vfs,
+ path: &Path,
+) -> anyhow::Result]