Use history recording and don't do anything permanent (#915)

This commit is contained in:
boatbomber
2024-05-30 12:28:58 -07:00
committed by GitHub
parent 3d4e387d35
commit 62f4a1f3c2
4 changed files with 59 additions and 17 deletions

View File

@@ -1,6 +1,7 @@
# Rojo Changelog # Rojo Changelog
## Unreleased Changes ## Unreleased Changes
* Updated Undo/Redo history to be more robust ([#915])
* Fixed removing trailing newlines ([#903]) * Fixed removing trailing newlines ([#903])
* Added Never option to Confirmation ([#893]) * Added Never option to Confirmation ([#893])
* Added popout diff visualizer for table properties like Attributes and Tags ([#834]) * Added popout diff visualizer for table properties like Attributes and Tags ([#834])
@@ -65,6 +66,7 @@
[#893]: https://github.com/rojo-rbx/rojo/pull/893 [#893]: https://github.com/rojo-rbx/rojo/pull/893
[#903]: https://github.com/rojo-rbx/rojo/pull/903 [#903]: https://github.com/rojo-rbx/rojo/pull/903
[#911]: https://github.com/rojo-rbx/rojo/pull/911 [#911]: https://github.com/rojo-rbx/rojo/pull/911
[#915]: https://github.com/rojo-rbx/rojo/pull/915
## [7.4.1] - February 20, 2024 ## [7.4.1] - February 20, 2024
* Made the `name` field optional on project files ([#870]) * Made the `name` field optional on project files ([#870])

View File

@@ -112,9 +112,12 @@ end
function InstanceMap:destroyInstance(instance) function InstanceMap:destroyInstance(instance)
local id = self.fromInstances[instance] local id = self.fromInstances[instance]
local descendants = instance:GetDescendants() local descendants = instance:GetDescendants()
instance:Destroy()
-- Because the user might want to Undo this change, we cannot use Destroy
-- since that locks that parent and prevents ChangeHistoryService from
-- ever bringing it back. Instead, we parent to nil.
instance.Parent = nil
-- After the instance is successfully destroyed, -- After the instance is successfully destroyed,
-- we can remove all the id mappings -- we can remove all the id mappings

View File

@@ -20,6 +20,11 @@ local setProperty = require(script.Parent.setProperty)
local function applyPatch(instanceMap, patch) local function applyPatch(instanceMap, patch)
local patchTimestamp = DateTime.now():FormatLocalTime("LTS", "en-us") local patchTimestamp = DateTime.now():FormatLocalTime("LTS", "en-us")
local historyRecording = ChangeHistoryService:TryBeginRecording("Rojo: Patch " .. patchTimestamp)
if not historyRecording then
-- There can only be one recording at a time
Log.debug("Failed to begin history recording for " .. patchTimestamp .. ". Another recording is in progress.")
end
-- Tracks any portions of the patch that could not be applied to the DOM. -- Tracks any portions of the patch that could not be applied to the DOM.
local unappliedPatch = PatchSet.newEmpty() local unappliedPatch = PatchSet.newEmpty()
@@ -62,6 +67,9 @@ local function applyPatch(instanceMap, patch)
if parentInstance == nil then if parentInstance == nil then
-- This would be peculiar. If you create an instance with no -- This would be peculiar. If you create an instance with no
-- parent, were you supposed to create it at all? -- parent, were you supposed to create it at all?
if historyRecording then
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
end
invariant( invariant(
"Cannot add an instance from a patch that has no parent.\nInstance {} with parent {}.\nState: {:#?}", "Cannot add an instance from a patch that has no parent.\nInstance {} with parent {}.\nState: {:#?}",
id, id,
@@ -164,10 +172,14 @@ local function applyPatch(instanceMap, patch)
end end
-- See you later, original instance. -- See you later, original instance.
--
-- Because the user might want to Undo this change, we cannot use Destroy
-- since that locks that parent and prevents ChangeHistoryService from
-- ever bringing it back. Instead, we parent to nil.
-- TODO: Can this fail? Some kinds of instance may not appreciate -- TODO: Can this fail? Some kinds of instance may not appreciate
-- being destroyed, like services. -- being reparented, like services.
instance:Destroy() instance.Parent = nil
-- This completes your rebuilding a plane mid-flight safety -- This completes your rebuilding a plane mid-flight safety
-- instruction. Please sit back, relax, and enjoy your flight. -- instruction. Please sit back, relax, and enjoy your flight.
@@ -214,7 +226,9 @@ local function applyPatch(instanceMap, patch)
end end
end end
ChangeHistoryService:SetWaypoint("Rojo: Patch " .. patchTimestamp) if historyRecording then
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
end
return unappliedPatch return unappliedPatch
end end

View File

@@ -4,25 +4,41 @@ return function()
local InstanceMap = require(script.Parent.Parent.InstanceMap) local InstanceMap = require(script.Parent.Parent.InstanceMap)
local PatchSet = require(script.Parent.Parent.PatchSet) local PatchSet = require(script.Parent.Parent.PatchSet)
local dummy = Instance.new("Folder") local container = Instance.new("Folder")
local function wasDestroyed(instance)
local tempContainer = Instance.new("Folder")
local function wasRemoved(instance)
-- If an instance was destroyed, its parent property is locked. -- If an instance was destroyed, its parent property is locked.
local ok = pcall(function() -- If an instance was removed, its parent property is nil.
-- We need to ensure we only remove, so that ChangeHistoryService can still Undo.
local isParentUnlocked = pcall(function()
local oldParent = instance.Parent local oldParent = instance.Parent
instance.Parent = dummy instance.Parent = tempContainer
instance.Parent = oldParent instance.Parent = oldParent
end) end)
return not ok return instance.Parent == nil and isParentUnlocked
end end
beforeEach(function()
container:ClearAllChildren()
end)
afterAll(function()
container:Destroy()
tempContainer:Destroy()
end)
it("should return an empty patch if given an empty patch", function() it("should return an empty patch if given an empty patch", function()
local patch = applyPatch(InstanceMap.new(), PatchSet.newEmpty()) local patch = applyPatch(InstanceMap.new(), PatchSet.newEmpty())
assert(PatchSet.isEmpty(patch), "expected remaining patch to be empty") assert(PatchSet.isEmpty(patch), "expected remaining patch to be empty")
end) end)
it("should destroy instances listed for remove", function() it("should remove instances listed for remove", function()
local root = Instance.new("Folder") local root = Instance.new("Folder")
root.Name = "ROOT"
root.Parent = container
local child = Instance.new("Folder") local child = Instance.new("Folder")
child.Name = "Child" child.Name = "Child"
@@ -38,14 +54,16 @@ return function()
local unapplied = applyPatch(instanceMap, patch) local unapplied = applyPatch(instanceMap, patch)
assert(PatchSet.isEmpty(unapplied), "expected remaining patch to be empty") assert(PatchSet.isEmpty(unapplied), "expected remaining patch to be empty")
assert(not wasDestroyed(root), "expected root to be left alone") assert(not wasRemoved(root), "expected root to be left alone")
assert(wasDestroyed(child), "expected child to be destroyed") assert(wasRemoved(child), "expected child to be removed")
instanceMap:stop() instanceMap:stop()
end) end)
it("should destroy IDs listed for remove", function() it("should remove IDs listed for remove", function()
local root = Instance.new("Folder") local root = Instance.new("Folder")
root.Name = "ROOT"
root.Parent = container
local child = Instance.new("Folder") local child = Instance.new("Folder")
child.Name = "Child" child.Name = "Child"
@@ -62,8 +80,8 @@ return function()
assert(PatchSet.isEmpty(unapplied), "expected remaining patch to be empty") assert(PatchSet.isEmpty(unapplied), "expected remaining patch to be empty")
expect(instanceMap:size()).to.equal(1) expect(instanceMap:size()).to.equal(1)
assert(not wasDestroyed(root), "expected root to be left alone") assert(not wasRemoved(root), "expected root to be left alone")
assert(wasDestroyed(child), "expected child to be destroyed") assert(wasRemoved(child), "expected child to be removed")
instanceMap:stop() instanceMap:stop()
end) end)
@@ -73,6 +91,8 @@ return function()
-- tests on reify, not here. -- tests on reify, not here.
local root = Instance.new("Folder") local root = Instance.new("Folder")
root.Name = "ROOT"
root.Parent = container
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
instanceMap:insert("ROOT", root) instanceMap:insert("ROOT", root)
@@ -113,6 +133,8 @@ return function()
it("should return unapplied additions when instances cannot be created", function() it("should return unapplied additions when instances cannot be created", function()
local root = Instance.new("Folder") local root = Instance.new("Folder")
root.Name = "ROOT"
root.Parent = container
local instanceMap = InstanceMap.new() local instanceMap = InstanceMap.new()
instanceMap:insert("ROOT", root) instanceMap:insert("ROOT", root)
@@ -159,6 +181,7 @@ return function()
it("should recreate instances when changedClassName is set, preserving children", function() it("should recreate instances when changedClassName is set, preserving children", function()
local root = Instance.new("Folder") local root = Instance.new("Folder")
root.Name = "Initial Root Name" root.Name = "Initial Root Name"
root.Parent = container
local child = Instance.new("Folder") local child = Instance.new("Folder")
child.Name = "Child" child.Name = "Child"