Add visual diffs to syncing (#603)

* Add user confirmation to initial sync

* Use "Accept" instead of "Confirm"

* Draw tree alphabetically for determinism

* Add diff table dropdown

* Add diff table to newly added objects

* Unblock keybind workflow

* Only show reject button when two way is enabled

* Try to patch back to the files when changes are rejected

* Improve text spacing of the prop diff table

* Skip user confirmation of perfect syncs

* Give instances names for debugging UI

* Optimize tree building

* Efficiency: dynamic virtual scrolling & lazy rendering

* Simplify virtual scroller logic and avoid wasteful rerenders

* Remove debug print

* Consistent naming

* Move new patch applied callback into accept

* Pcall archivable

* Keybinds open popup diff window

* Theme rows in diff

* Remove relic of prototype

* Color value visuals and better component name

* changeBatcher is not needed when no sync is active

* Simplify popup roact entrypoint

* Alphabetical prop lists and refactor

* Add a stroke to color blot for contrast

* Make color blots animate transparency with the rest of the page

* StyLua formatting on newly added files

* Remove wasteful table

* Fix diffing custom properties

* Display tables more meaningfully

* Allow children in the button components

* Create a rough tooltip component

* Add tooltips to buttons

* Use provider+trigger schema to avoid tooltip ZIndex issues

* Add triangle point to tooltip

* Tooltip underneath instead of covering

* Cancel hovers when unmounting

* Allow multiple canvases from one provider

* Display above or below depending on available space

* Move patch equality to PatchSet.isEqual

* Use Container

* Remove old submodules

* Reduce false positives in diff

* Add debug log

* Fuzzy equals CFrame in diffs to avoid floating point in

* Fix decodeValue usage

* Support the .changedName patches

* Fix content overlapping border

* Fix tooltip tail alignment

* Fix tooltip text fit

* Whoops, fix it properly

* Move PatchVisualizer to Components

* Provide Connected info with full patch data

* Avoid implicit nil return

* Add patch visualizer to connected page

* Make Current column invisible when visualizing applied patches

* Avoid floating point diffs in a numbers and vectors
This commit is contained in:
boatbomber
2023-04-01 23:17:23 -04:00
committed by GitHub
parent 7994bc4909
commit b5ed952d5c
19 changed files with 1618 additions and 81 deletions

View File

@@ -5,8 +5,10 @@ local Packages = script.Parent.Parent.Packages
local Log = require(Packages.Log)
local Fmt = require(Packages.Fmt)
local t = require(Packages.t)
local Promise = require(Packages.Promise)
local ChangeBatcher = require(script.Parent.ChangeBatcher)
local encodePatchUpdate = require(script.Parent.ChangeBatcher.encodePatchUpdate)
local InstanceMap = require(script.Parent.InstanceMap)
local PatchSet = require(script.Parent.PatchSet)
local Reconciler = require(script.Parent.Reconciler)
@@ -123,6 +125,10 @@ function ServeSession:onStatusChanged(callback)
self.__statusChangedCallback = callback
end
function ServeSession:setConfirmCallback(callback)
self.__userConfirmCallback = callback
end
function ServeSession:onPatchApplied(callback)
self.__patchAppliedCallback = callback
end
@@ -132,13 +138,12 @@ function ServeSession:start()
self.__apiContext:connect()
:andThen(function(serverInfo)
self:__setStatus(Status.Connected, serverInfo.projectName)
self:__applyGameAndPlaceId(serverInfo)
local rootInstanceId = serverInfo.rootInstanceId
return self:__initialSync(rootInstanceId)
return self:__initialSync(serverInfo)
:andThen(function()
self:__setStatus(Status.Connected, serverInfo.projectName)
return self:__mainSyncLoop()
end)
end)
@@ -202,8 +207,8 @@ function ServeSession:__onActiveScriptChanged(activeScript)
self.__apiContext:open(scriptId)
end
function ServeSession:__initialSync(rootInstanceId)
return self.__apiContext:read({ rootInstanceId })
function ServeSession:__initialSync(serverInfo)
return self.__apiContext:read({ serverInfo.rootInstanceId })
:andThen(function(readResponseBody)
-- Tell the API Context that we're up-to-date with the version of
-- the tree defined in this response.
@@ -212,14 +217,14 @@ function ServeSession:__initialSync(rootInstanceId)
-- For any instances that line up with the Rojo server's view, start
-- tracking them in the reconciler.
Log.trace("Matching existing Roblox instances to Rojo IDs")
self.__reconciler:hydrate(readResponseBody.instances, rootInstanceId, game)
self.__reconciler:hydrate(readResponseBody.instances, serverInfo.rootInstanceId, game)
-- Calculate the initial patch to apply to the DataModel to catch us
-- up to what Rojo thinks the place should look like.
Log.trace("Computing changes that plugin needs to make to catch up to server...")
local success, catchUpPatch = self.__reconciler:diff(
readResponseBody.instances,
rootInstanceId,
serverInfo.rootInstanceId,
game
)
@@ -229,19 +234,50 @@ function ServeSession:__initialSync(rootInstanceId)
Log.trace("Computed hydration patch: {:#?}", debugPatch(catchUpPatch))
-- TODO: Prompt user to notify them of this patch, since it's
-- effectively a conflict between the Rojo server and the client. In
-- the future, we'll ask which changes the user wants to keep.
local unappliedPatch = self.__reconciler:applyPatch(catchUpPatch)
if not PatchSet.isEmpty(unappliedPatch) then
Log.warn("Could not apply all changes requested by the Rojo server:\n{}",
PatchSet.humanSummary(self.__instanceMap, unappliedPatch))
local userDecision = "Accept"
if self.__userConfirmCallback ~= nil then
userDecision = self.__userConfirmCallback(self.__instanceMap, catchUpPatch, serverInfo)
end
if self.__patchAppliedCallback then
pcall(self.__patchAppliedCallback, catchUpPatch, unappliedPatch)
if userDecision == "Abort" then
return Promise.reject("Aborted Rojo sync operation")
elseif userDecision == "Reject" and self.__twoWaySync then
-- The user wants their studio DOM to write back to their Rojo DOM
-- so we will reverse the patch and send it back
local inversePatch = PatchSet.newEmpty()
-- Send back the current properties
for _, change in catchUpPatch.updated do
local instance = self.__instanceMap.fromIds[change.id]
if not instance then continue end
local update = encodePatchUpdate(instance, change.id, change.changedProperties)
table.insert(inversePatch.updated, update)
end
-- Add the removed instances back to Rojo
-- selene:allow(empty_if, unused_variable)
for _, instance in catchUpPatch.removed do
-- TODO: Generate ID for our instance and add it to inversePatch.added
end
-- Remove the additions we've rejected
for id, _change in catchUpPatch.added do
table.insert(inversePatch.removed, id)
end
self.__apiContext:write(inversePatch)
elseif userDecision == "Accept" then
local unappliedPatch = self.__reconciler:applyPatch(catchUpPatch)
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
if self.__patchAppliedCallback then
pcall(self.__patchAppliedCallback, catchUpPatch, unappliedPatch)
end
end
end)
end