forked from rojo-rbx/rojo
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
8053909bd0
|
|||
|
|
d08780fc14 | ||
|
|
b89cc7f398 | ||
|
|
42568b9709 | ||
|
|
87f58e0a55 | ||
|
|
a61a1bef55 | ||
|
|
a99e877b7c | ||
|
|
93e9c51204 | ||
|
|
015b5bda14 | ||
|
|
2b47861a4f | ||
|
|
9b5a07191b | ||
|
|
071b6e7e23 | ||
|
|
31ec216a95 | ||
|
|
ea70d89291 | ||
|
|
03410ced6d | ||
|
|
825726c883 | ||
|
|
54e63d88d4 | ||
|
|
4018c97cb6 | ||
|
|
d0b029f995 | ||
|
|
aabe6d11b2 | ||
|
|
181cc37744 | ||
|
|
cd78f5c02c |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@1.83.0
|
||||
uses: dtolnay/rust-toolchain@1.88.0
|
||||
|
||||
- name: Restore Rust Cache
|
||||
uses: actions/cache/restore@v4
|
||||
|
||||
674
CHANGELOG.md
674
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
1678
Cargo.lock
generated
1678
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
31
Cargo.toml
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "7.6.0"
|
||||
rust-version = "1.83"
|
||||
version = "7.7.0-rc.1"
|
||||
rust-version = "1.88"
|
||||
authors = [
|
||||
"Lucien Greathouse <me@lpghatguy.com>",
|
||||
"Micah Reid <git@dekkonot.com>",
|
||||
@@ -46,20 +46,22 @@ name = "build"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
memofs = { version = "0.3.0", path = "crates/memofs" }
|
||||
memofs = { version = "0.3.1", path = "crates/memofs" }
|
||||
|
||||
# These dependencies can be uncommented when working on rbx-dom simultaneously
|
||||
# rbx_binary = { path = "../rbx-dom/rbx_binary" }
|
||||
# rbx_binary = { path = "../rbx-dom/rbx_binary", features = [
|
||||
# "unstable_text_format",
|
||||
# ] }
|
||||
# rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" }
|
||||
# rbx_reflection = { path = "../rbx-dom/rbx_reflection" }
|
||||
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
||||
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
||||
|
||||
rbx_binary = "2.0.0"
|
||||
rbx_dom_weak = "4.0.0"
|
||||
rbx_reflection = "6.0.0"
|
||||
rbx_reflection_database = "2.0.0"
|
||||
rbx_xml = "2.0.0"
|
||||
rbx_binary = { version = "2.0.1", features = ["unstable_text_format"] }
|
||||
rbx_dom_weak = "4.1.0"
|
||||
rbx_reflection = "6.1.0"
|
||||
rbx_reflection_database = "2.0.2"
|
||||
rbx_xml = "2.0.1"
|
||||
|
||||
anyhow = "1.0.80"
|
||||
backtrace = "0.3.69"
|
||||
@@ -72,6 +74,7 @@ futures = "0.3.30"
|
||||
globset = "0.4.14"
|
||||
humantime = "2.1.0"
|
||||
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
|
||||
hyper-tungstenite = "0.11.0"
|
||||
jod-thread = "0.1.2"
|
||||
log = "0.4.21"
|
||||
num_cpus = "1.16.0"
|
||||
@@ -85,17 +88,23 @@ reqwest = { version = "0.11.24", default-features = false, features = [
|
||||
ritz = "0.1.0"
|
||||
roblox_install = "1.0.0"
|
||||
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.114"
|
||||
serde_json = "1.0.145"
|
||||
jsonc-parser = { version = "0.27.0", features = ["serde"] }
|
||||
strum = { version = "0.27", features = ["derive"] }
|
||||
toml = "0.5.11"
|
||||
termcolor = "1.4.1"
|
||||
thiserror = "1.0.57"
|
||||
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread"] }
|
||||
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
||||
clap = { version = "3.2.25", features = ["derive"] }
|
||||
profiling = "1.0.15"
|
||||
yaml-rust2 = "0.10.3"
|
||||
data-encoding = "2.8.0"
|
||||
|
||||
blake3 = "1.5.0"
|
||||
float-cmp = "0.9.0"
|
||||
indexmap = { version = "2.10.0", features = ["serde"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.10.1"
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
|
||||
|
||||
Pull requests are welcome!
|
||||
|
||||
Rojo supports Rust 1.83 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||
Rojo supports Rust 1.88 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||
|
||||
## License
|
||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
|
||||
sourcemap.json
|
||||
|
||||
@@ -4,3 +4,5 @@
|
||||
# Roblox Studio lock files
|
||||
/*.rbxlx.lock
|
||||
/*.rbxl.lock
|
||||
|
||||
sourcemap.json
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Plugin model files
|
||||
/{project_name}.rbxmx
|
||||
/{project_name}.rbxm
|
||||
|
||||
sourcemap.json
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
## Unreleased Changes
|
||||
|
||||
## 0.3.1 (2025-11-27)
|
||||
* Added `Vfs::exists`. [#1169]
|
||||
* Added `create_dir` and `create_dir_all` to allow creating directories. [#937]
|
||||
|
||||
[#1169]: https://github.com/rojo-rbx/rojo/pull/1169
|
||||
[#937]: https://github.com/rojo-rbx/rojo/pull/937
|
||||
|
||||
## 0.3.0 (2024-03-15)
|
||||
* Changed `StdBackend` file watching component to use minimal recursive watches. [#830]
|
||||
* Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "memofs"
|
||||
description = "Virtual filesystem with configurable backends."
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
authors = [
|
||||
"Lucien Greathouse <me@lpghatguy.com>",
|
||||
"Micah Reid <git@dekkonot.com>",
|
||||
|
||||
@@ -157,6 +157,11 @@ impl VfsBackend for InMemoryFs {
|
||||
)
|
||||
}
|
||||
|
||||
fn exists(&mut self, path: &Path) -> io::Result<bool> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
Ok(inner.entries.contains_key(path))
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
@@ -176,6 +181,21 @@ impl VfsBackend for InMemoryFs {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
|
||||
}
|
||||
|
||||
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
let mut path_buf = path.to_path_buf();
|
||||
while let Some(parent) = path_buf.parent() {
|
||||
inner.load_snapshot(parent.to_path_buf(), VfsSnapshot::empty_dir())?;
|
||||
path_buf.pop();
|
||||
}
|
||||
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
|
||||
}
|
||||
|
||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
|
||||
|
||||
@@ -70,7 +70,10 @@ impl<T> IoResultExt<T> for io::Result<T> {
|
||||
pub trait VfsBackend: sealed::Sealed + Send + 'static {
|
||||
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>>;
|
||||
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>;
|
||||
fn exists(&mut self, path: &Path) -> io::Result<bool>;
|
||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir>;
|
||||
fn create_dir(&mut self, path: &Path) -> io::Result<()>;
|
||||
fn create_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
||||
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
|
||||
fn remove_file(&mut self, path: &Path) -> io::Result<()>;
|
||||
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
|
||||
@@ -173,6 +176,11 @@ impl VfsInner {
|
||||
Ok(Arc::new(contents_str.into()))
|
||||
}
|
||||
|
||||
fn exists<P: AsRef<Path>>(&mut self, path: P) -> io::Result<bool> {
|
||||
let path = path.as_ref();
|
||||
self.backend.exists(path)
|
||||
}
|
||||
|
||||
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let contents = contents.as_ref();
|
||||
@@ -190,6 +198,16 @@ impl VfsInner {
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.backend.create_dir(path)
|
||||
}
|
||||
|
||||
fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.backend.create_dir_all(path)
|
||||
}
|
||||
|
||||
fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let _ = self.backend.unwatch(path);
|
||||
@@ -326,6 +344,42 @@ impl Vfs {
|
||||
self.inner.lock().unwrap().read_dir(path)
|
||||
}
|
||||
|
||||
/// Return whether the given path exists.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::exists`][std::fs::exists].
|
||||
///
|
||||
/// [std::fs::exists]: https://doc.rust-lang.org/stable/std/fs/fn.exists.html
|
||||
#[inline]
|
||||
pub fn exists<P: AsRef<Path>>(&self, path: P) -> io::Result<bool> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().exists(path)
|
||||
}
|
||||
|
||||
/// Creates a directory at the provided location.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
|
||||
/// Similiar to that function, this function will fail if the parent of the
|
||||
/// path does not exist.
|
||||
///
|
||||
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
|
||||
#[inline]
|
||||
pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().create_dir(path)
|
||||
}
|
||||
|
||||
/// Creates a directory at the provided location, recursively creating
|
||||
/// all parent components if they are missing.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
|
||||
///
|
||||
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
|
||||
#[inline]
|
||||
pub fn create_dir_all<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().create_dir_all(path)
|
||||
}
|
||||
|
||||
/// Remove a file.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
||||
@@ -428,6 +482,31 @@ impl VfsLock<'_> {
|
||||
self.inner.read_dir(path)
|
||||
}
|
||||
|
||||
/// Creates a directory at the provided location.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
|
||||
/// Similiar to that function, this function will fail if the parent of the
|
||||
/// path does not exist.
|
||||
///
|
||||
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
|
||||
#[inline]
|
||||
pub fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.inner.create_dir(path)
|
||||
}
|
||||
|
||||
/// Creates a directory at the provided location, recursively creating
|
||||
/// all parent components if they are missing.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
|
||||
///
|
||||
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
|
||||
#[inline]
|
||||
pub fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
|
||||
let path = path.as_ref();
|
||||
self.inner.create_dir_all(path)
|
||||
}
|
||||
|
||||
/// Remove a file.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
|
||||
|
||||
@@ -22,10 +22,22 @@ impl VfsBackend for NoopBackend {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn exists(&mut self, _path: &Path) -> io::Result<bool> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn create_dir(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn create_dir_all(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
@@ -63,6 +63,10 @@ impl VfsBackend for StdBackend {
|
||||
fs_err::write(path, data)
|
||||
}
|
||||
|
||||
fn exists(&mut self, path: &Path) -> io::Result<bool> {
|
||||
std::fs::exists(path)
|
||||
}
|
||||
|
||||
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
|
||||
let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
|
||||
let mut entries = entries?;
|
||||
@@ -78,6 +82,14 @@ impl VfsBackend for StdBackend {
|
||||
})
|
||||
}
|
||||
|
||||
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
|
||||
fs_err::create_dir(path)
|
||||
}
|
||||
|
||||
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
|
||||
fs_err::create_dir_all(path)
|
||||
}
|
||||
|
||||
fn remove_file(&mut self, path: &Path) -> io::Result<()> {
|
||||
fs_err::remove_file(path)
|
||||
}
|
||||
|
||||
Submodule plugin/Packages/Highlighter updated: e0d061449e...c12c488dad
@@ -1 +1 @@
|
||||
7.6.0
|
||||
7.7.0-rc.1
|
||||
@@ -25,7 +25,7 @@
|
||||
local function defaultTableDebug(buffer, input)
|
||||
buffer:writeRaw("{")
|
||||
|
||||
for key, value in pairs(input) do
|
||||
for key, value in input do
|
||||
buffer:write("[{:?}] = {:?}", key, value)
|
||||
|
||||
if next(input, key) ~= nil then
|
||||
@@ -50,7 +50,7 @@ local function defaultTableDebugExtended(buffer, input)
|
||||
buffer:writeLineRaw("{")
|
||||
buffer:indent()
|
||||
|
||||
for key, value in pairs(input) do
|
||||
for key, value in input do
|
||||
buffer:writeLine("[{:?}] = {:#?},", key, value)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,139 +1,10 @@
|
||||
-- Thanks to Tiffany352 for this base64 implementation!
|
||||
|
||||
local floor = math.floor
|
||||
local char = string.char
|
||||
|
||||
local function encodeBase64(str)
|
||||
local out = {}
|
||||
local nOut = 0
|
||||
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
local strLen = #str
|
||||
|
||||
-- 3 octets become 4 hextets
|
||||
for i = 1, strLen - 2, 3 do
|
||||
local b1, b2, b3 = str:byte(i, i + 3)
|
||||
local word = b3 + b2 * 256 + b1 * 256 * 256
|
||||
|
||||
local h4 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h3 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h2 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h1 = word % 64 + 1
|
||||
|
||||
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||
out[nOut + 3] = alphabet:sub(h3, h3)
|
||||
out[nOut + 4] = alphabet:sub(h4, h4)
|
||||
nOut = nOut + 4
|
||||
end
|
||||
|
||||
local remainder = strLen % 3
|
||||
|
||||
if remainder == 2 then
|
||||
-- 16 input bits -> 3 hextets (2 full, 1 partial)
|
||||
local b1, b2 = str:byte(-2, -1)
|
||||
-- partial is 4 bits long, leaving 2 bits of zero padding ->
|
||||
-- offset = 4
|
||||
local word = b2 * 4 + b1 * 4 * 256
|
||||
|
||||
local h3 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h2 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h1 = word % 64 + 1
|
||||
|
||||
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||
out[nOut + 3] = alphabet:sub(h3, h3)
|
||||
out[nOut + 4] = "="
|
||||
elseif remainder == 1 then
|
||||
-- 8 input bits -> 2 hextets (2 full, 1 partial)
|
||||
local b1 = str:byte(-1, -1)
|
||||
-- partial is 2 bits long, leaving 4 bits of zero padding ->
|
||||
-- offset = 16
|
||||
local word = b1 * 16
|
||||
|
||||
local h2 = word % 64 + 1
|
||||
word = floor(word / 64)
|
||||
local h1 = word % 64 + 1
|
||||
|
||||
out[nOut + 1] = alphabet:sub(h1, h1)
|
||||
out[nOut + 2] = alphabet:sub(h2, h2)
|
||||
out[nOut + 3] = "="
|
||||
out[nOut + 4] = "="
|
||||
end
|
||||
-- if the remainder is 0, then no work is needed
|
||||
|
||||
return table.concat(out, "")
|
||||
end
|
||||
|
||||
local function decodeBase64(str)
|
||||
local out = {}
|
||||
local nOut = 0
|
||||
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
local strLen = #str
|
||||
local acc = 0
|
||||
local nAcc = 0
|
||||
|
||||
local alphabetLut = {}
|
||||
for i = 1, #alphabet do
|
||||
alphabetLut[alphabet:sub(i, i)] = i - 1
|
||||
end
|
||||
|
||||
-- 4 hextets become 3 octets
|
||||
for i = 1, strLen do
|
||||
local ch = str:sub(i, i)
|
||||
local byte = alphabetLut[ch]
|
||||
if byte then
|
||||
acc = acc * 64 + byte
|
||||
nAcc = nAcc + 1
|
||||
end
|
||||
|
||||
if nAcc == 4 then
|
||||
local b3 = acc % 256
|
||||
acc = floor(acc / 256)
|
||||
local b2 = acc % 256
|
||||
acc = floor(acc / 256)
|
||||
local b1 = acc % 256
|
||||
|
||||
out[nOut + 1] = char(b1)
|
||||
out[nOut + 2] = char(b2)
|
||||
out[nOut + 3] = char(b3)
|
||||
nOut = nOut + 3
|
||||
nAcc = 0
|
||||
acc = 0
|
||||
end
|
||||
end
|
||||
|
||||
if nAcc == 3 then
|
||||
-- 3 hextets -> 16 bit output
|
||||
acc = acc * 64
|
||||
acc = floor(acc / 256)
|
||||
local b2 = acc % 256
|
||||
acc = floor(acc / 256)
|
||||
local b1 = acc % 256
|
||||
|
||||
out[nOut + 1] = char(b1)
|
||||
out[nOut + 2] = char(b2)
|
||||
elseif nAcc == 2 then
|
||||
-- 2 hextets -> 8 bit output
|
||||
acc = acc * 64
|
||||
acc = floor(acc / 256)
|
||||
acc = acc * 64
|
||||
acc = floor(acc / 256)
|
||||
local b1 = acc % 256
|
||||
|
||||
out[nOut + 1] = char(b1)
|
||||
elseif nAcc == 1 then
|
||||
error("Base64 has invalid length")
|
||||
end
|
||||
|
||||
return table.concat(out, "")
|
||||
end
|
||||
local EncodingService = game:GetService("EncodingService")
|
||||
|
||||
return {
|
||||
decode = decodeBase64,
|
||||
encode = encodeBase64,
|
||||
decode = function(input: string)
|
||||
return buffer.tostring(EncodingService:Base64Decode(buffer.fromstring(input)))
|
||||
end,
|
||||
encode = function(input: string)
|
||||
return buffer.tostring(EncodingService:Base64Encode(buffer.fromstring(input)))
|
||||
end,
|
||||
}
|
||||
|
||||
@@ -208,4 +208,30 @@ return {
|
||||
end,
|
||||
},
|
||||
},
|
||||
StyleRule = {
|
||||
PropertiesSerialize = {
|
||||
read = function(instance: StyleRule)
|
||||
return true, instance:GetProperties()
|
||||
end,
|
||||
write = function(instance: StyleRule, _, value: { [any]: any })
|
||||
if typeof(value) ~= "table" then
|
||||
return false, Error.new(Error.Kind.CannotParseBinaryString)
|
||||
end
|
||||
|
||||
local existing = instance:GetProperties()
|
||||
|
||||
for itemName, itemValue in pairs(value) do
|
||||
instance:SetProperty(itemName, itemValue)
|
||||
end
|
||||
|
||||
for existingItemName in pairs(existing) do
|
||||
if value[existingItemName] == nil then
|
||||
instance:SetProperty(existingItemName, nil)
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
local Packages = script.Parent.Parent.Packages
|
||||
local HttpService = game:GetService("HttpService")
|
||||
local Http = require(Packages.Http)
|
||||
local Log = require(Packages.Log)
|
||||
local Promise = require(Packages.Promise)
|
||||
@@ -9,7 +10,7 @@ local Version = require(script.Parent.Version)
|
||||
|
||||
local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
|
||||
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
|
||||
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
|
||||
local validateApiSocketPacket = Types.ifEnabled(Types.ApiSocketPacket)
|
||||
local validateApiSerialize = Types.ifEnabled(Types.ApiSerializeResponse)
|
||||
local validateApiRefPatch = Types.ifEnabled(Types.ApiRefPatchResponse)
|
||||
|
||||
@@ -99,6 +100,7 @@ function ApiContext.new(baseUrl)
|
||||
__baseUrl = baseUrl,
|
||||
__sessionId = nil,
|
||||
__messageCursor = -1,
|
||||
__wsClient = nil,
|
||||
__connected = true,
|
||||
__activeRequests = {},
|
||||
}
|
||||
@@ -126,6 +128,12 @@ function ApiContext:disconnect()
|
||||
request:cancel()
|
||||
end
|
||||
self.__activeRequests = {}
|
||||
|
||||
if self.__wsClient then
|
||||
Log.trace("Closing WebSocket client")
|
||||
self.__wsClient:Close()
|
||||
end
|
||||
self.__wsClient = nil
|
||||
end
|
||||
|
||||
function ApiContext:setMessageCursor(index)
|
||||
@@ -207,38 +215,65 @@ function ApiContext:write(patch)
|
||||
end)
|
||||
end
|
||||
|
||||
function ApiContext:retrieveMessages()
|
||||
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
|
||||
function ApiContext:connectWebSocket(packetHandlers)
|
||||
local url = ("%s/api/socket/%s"):format(self.__baseUrl, self.__messageCursor)
|
||||
-- Convert HTTP/HTTPS URL to WS/WSS
|
||||
url = url:gsub("^http://", "ws://"):gsub("^https://", "wss://")
|
||||
|
||||
local function sendRequest()
|
||||
local request = Http.get(url):catch(function(err)
|
||||
if err.type == Http.Error.Kind.Timeout and self.__connected then
|
||||
return sendRequest()
|
||||
return Promise.new(function(resolve, reject)
|
||||
local success, wsClient =
|
||||
pcall(HttpService.CreateWebStreamClient, HttpService, Enum.WebStreamClientType.WebSocket, {
|
||||
Url = url,
|
||||
})
|
||||
if not success then
|
||||
reject("Failed to create WebSocket client: " .. tostring(wsClient))
|
||||
return
|
||||
end
|
||||
self.__wsClient = wsClient
|
||||
|
||||
local closed, errored, received
|
||||
|
||||
received = self.__wsClient.MessageReceived:Connect(function(msg)
|
||||
local data = Http.jsonDecode(msg)
|
||||
if data.sessionId ~= self.__sessionId then
|
||||
Log.warn("Received message with wrong session ID; ignoring")
|
||||
return
|
||||
end
|
||||
|
||||
return Promise.reject(err)
|
||||
assert(validateApiSocketPacket(data))
|
||||
|
||||
Log.trace("Received websocket packet: {:#?}", data)
|
||||
|
||||
local handler = packetHandlers[data.packetType]
|
||||
if handler then
|
||||
local ok, err = pcall(handler, data.body)
|
||||
if not ok then
|
||||
Log.error("Error in WebSocket packet handler for type '%s': %s", data.packetType, err)
|
||||
end
|
||||
else
|
||||
Log.warn("No handler for WebSocket packet type '%s'", data.packetType)
|
||||
end
|
||||
end)
|
||||
|
||||
Log.trace("Tracking request {}", request)
|
||||
self.__activeRequests[request] = true
|
||||
closed = self.__wsClient.Closed:Connect(function()
|
||||
closed:Disconnect()
|
||||
errored:Disconnect()
|
||||
received:Disconnect()
|
||||
|
||||
return request:finally(function(...)
|
||||
Log.trace("Cleaning up request {}", request)
|
||||
self.__activeRequests[request] = nil
|
||||
return ...
|
||||
if self.__connected then
|
||||
reject("WebSocket connection closed unexpectedly")
|
||||
else
|
||||
resolve()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return sendRequest():andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||
if body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
errored = self.__wsClient.Error:Connect(function(code, msg)
|
||||
closed:Disconnect()
|
||||
errored:Disconnect()
|
||||
received:Disconnect()
|
||||
|
||||
assert(validateApiSubscribe(body))
|
||||
|
||||
self:setMessageCursor(body.messageCursor)
|
||||
|
||||
return body.messages
|
||||
reject("WebSocket error: " .. code .. " - " .. msg)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
local StudioService = game:GetService("StudioService")
|
||||
local AssetService = game:GetService("AssetService")
|
||||
|
||||
type CachedImageInfo = {
|
||||
pixels: buffer,
|
||||
size: Vector2,
|
||||
}
|
||||
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
@@ -11,44 +16,71 @@ local e = Roact.createElement
|
||||
|
||||
local EditableImage = require(Plugin.App.Components.EditableImage)
|
||||
|
||||
local imageCache = {}
|
||||
local function getImageSizeAndPixels(image)
|
||||
if not imageCache[image] then
|
||||
local editableImage = AssetService:CreateEditableImageAsync(image)
|
||||
local imageCache: { [string]: CachedImageInfo } = {}
|
||||
|
||||
local function cloneBuffer(b: buffer): buffer
|
||||
local newBuffer = buffer.create(buffer.len(b))
|
||||
buffer.copy(newBuffer, 0, b)
|
||||
return newBuffer
|
||||
end
|
||||
|
||||
local function getImageSizeAndPixels(image: string): (Vector2, buffer)
|
||||
local cachedImage = imageCache[image]
|
||||
|
||||
if not cachedImage then
|
||||
local editableImage = AssetService:CreateEditableImageAsync(Content.fromUri(image))
|
||||
local size = editableImage.Size
|
||||
local pixels = editableImage:ReadPixelsBuffer(Vector2.zero, size)
|
||||
imageCache[image] = {
|
||||
Size = editableImage.Size,
|
||||
Pixels = editableImage:ReadPixels(Vector2.zero, editableImage.Size),
|
||||
pixels = pixels,
|
||||
size = size,
|
||||
}
|
||||
|
||||
return size, cloneBuffer(pixels)
|
||||
end
|
||||
|
||||
return imageCache[image].Size, table.clone(imageCache[image].Pixels)
|
||||
return cachedImage.size, cloneBuffer(cachedImage.pixels)
|
||||
end
|
||||
|
||||
local function getRecoloredClassIcon(className, color)
|
||||
local iconProps = StudioService:GetClassIcon(className)
|
||||
|
||||
if iconProps and color then
|
||||
local success, editableImageSize, editableImagePixels = pcall(function()
|
||||
local size, pixels = getImageSizeAndPixels(iconProps.Image)
|
||||
--stylua: ignore
|
||||
local success, editableImageSize, editableImagePixels = pcall(function(_iconProps: { [any]: any }, _color: Color3): (Vector2, buffer)
|
||||
local size, pixels = getImageSizeAndPixels(_iconProps.Image)
|
||||
local pixelsLen = buffer.len(pixels)
|
||||
|
||||
local minVal, maxVal = math.huge, -math.huge
|
||||
for i = 1, #pixels, 4 do
|
||||
if pixels[i + 3] == 0 then
|
||||
|
||||
for i = 0, pixelsLen, 4 do
|
||||
if buffer.readu8(pixels, i + 3) == 0 then
|
||||
continue
|
||||
end
|
||||
local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
|
||||
local pixelVal = math.max(
|
||||
buffer.readu8(pixels, i),
|
||||
buffer.readu8(pixels, i + 1),
|
||||
buffer.readu8(pixels, i + 2)
|
||||
)
|
||||
|
||||
minVal = math.min(minVal, pixelVal)
|
||||
maxVal = math.max(maxVal, pixelVal)
|
||||
end
|
||||
|
||||
local hue, sat, val = color:ToHSV()
|
||||
for i = 1, #pixels, 4 do
|
||||
if pixels[i + 3] == 0 then
|
||||
local hue, sat, val = _color:ToHSV()
|
||||
|
||||
for i = 0, pixelsLen, 4 do
|
||||
if buffer.readu8(pixels, i + 3) == 0 then
|
||||
continue
|
||||
end
|
||||
local gIndex = i + 1
|
||||
local bIndex = i + 2
|
||||
|
||||
local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
|
||||
local pixelVal = math.max(
|
||||
buffer.readu8(pixels, i),
|
||||
buffer.readu8(pixels, gIndex),
|
||||
buffer.readu8(pixels, bIndex)
|
||||
)
|
||||
local newVal = val
|
||||
if minVal < maxVal then
|
||||
-- Remap minVal - maxVal to val*0.9 - val
|
||||
@@ -56,10 +88,12 @@ local function getRecoloredClassIcon(className, color)
|
||||
end
|
||||
|
||||
local newPixelColor = Color3.fromHSV(hue, sat, newVal)
|
||||
pixels[i], pixels[i + 1], pixels[i + 2] = newPixelColor.R, newPixelColor.G, newPixelColor.B
|
||||
buffer.writeu8(pixels, i, newPixelColor.R)
|
||||
buffer.writeu8(pixels, gIndex, newPixelColor.G)
|
||||
buffer.writeu8(pixels, bIndex, newPixelColor.B)
|
||||
end
|
||||
return size, pixels
|
||||
end)
|
||||
end, iconProps, color)
|
||||
if success then
|
||||
iconProps.EditableImagePixels = editableImagePixels
|
||||
iconProps.EditableImageSize = editableImageSize
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
local Rojo = script:FindFirstAncestor("Rojo")
|
||||
local Plugin = Rojo.Plugin
|
||||
local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
local Highlighter = require(Packages.Highlighter)
|
||||
Highlighter.matchStudioSettings()
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
|
||||
local CodeLabel = Roact.PureComponent:extend("CodeLabel")
|
||||
|
||||
function CodeLabel:init()
|
||||
self.labelRef = Roact.createRef()
|
||||
self.highlightsRef = Roact.createRef()
|
||||
end
|
||||
|
||||
function CodeLabel:didMount()
|
||||
Highlighter.highlight({
|
||||
textObject = self.labelRef:getValue(),
|
||||
})
|
||||
self:updateHighlights()
|
||||
end
|
||||
|
||||
function CodeLabel:didUpdate()
|
||||
self:updateHighlights()
|
||||
end
|
||||
|
||||
function CodeLabel:updateHighlights()
|
||||
local highlights = self.highlightsRef:getValue()
|
||||
if not highlights then
|
||||
return
|
||||
end
|
||||
|
||||
for _, lineLabel in highlights:GetChildren() do
|
||||
local lineNum = tonumber(string.match(lineLabel.Name, "%d+") or "0")
|
||||
lineLabel.BackgroundColor3 = self.props.lineBackground
|
||||
lineLabel.BorderSizePixel = 0
|
||||
lineLabel.BackgroundTransparency = if self.props.markedLines[lineNum] then 0.25 else 1
|
||||
end
|
||||
end
|
||||
|
||||
function CodeLabel:render()
|
||||
return Theme.with(function(theme)
|
||||
return e("TextLabel", {
|
||||
Size = self.props.size,
|
||||
Position = self.props.position,
|
||||
Text = self.props.text,
|
||||
BackgroundTransparency = 1,
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Code,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
TextColor3 = Color3.fromRGB(255, 255, 255),
|
||||
[Roact.Ref] = self.labelRef,
|
||||
}, {
|
||||
SyntaxHighlights = e("Folder", {
|
||||
[Roact.Ref] = self.highlightsRef,
|
||||
}),
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return CodeLabel
|
||||
@@ -12,7 +12,8 @@ function EditableImage:init()
|
||||
end
|
||||
|
||||
function EditableImage:writePixels()
|
||||
local image = self.ref.current
|
||||
local image = self.ref.current :: EditableImage
|
||||
|
||||
if not image then
|
||||
return
|
||||
end
|
||||
@@ -20,7 +21,7 @@ function EditableImage:writePixels()
|
||||
return
|
||||
end
|
||||
|
||||
image:WritePixels(Vector2.zero, self.props.size, self.props.pixels)
|
||||
image:WritePixelsBuffer(Vector2.zero, self.props.size, self.props.pixels)
|
||||
end
|
||||
|
||||
function EditableImage:render()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
--!strict
|
||||
--[[
|
||||
Based on DiffMatchPatch by Neil Fraser.
|
||||
https://github.com/google/diff-match-patch
|
||||
@@ -67,8 +68,187 @@ function StringDiff.findDiffs(text1: string, text2: string): Diffs
|
||||
end
|
||||
|
||||
-- Cleanup the diff
|
||||
diffs = StringDiff._cleanupSemantic(diffs)
|
||||
diffs = StringDiff._reorderAndMerge(diffs)
|
||||
|
||||
-- Remove any empty diffs
|
||||
local cursor = 1
|
||||
while cursor and diffs[cursor] do
|
||||
if diffs[cursor].value == "" then
|
||||
table.remove(diffs, cursor)
|
||||
else
|
||||
cursor += 1
|
||||
end
|
||||
end
|
||||
|
||||
return diffs
|
||||
end
|
||||
|
||||
function StringDiff._computeDiff(text1: string, text2: string): Diffs
|
||||
-- Assumes that the prefix and suffix have already been trimmed off
|
||||
-- and shortcut returns have been made so these texts must be different
|
||||
|
||||
local text1Length, text2Length = #text1, #text2
|
||||
|
||||
if text1Length == 0 then
|
||||
-- It's simply inserting all of text2 into text1
|
||||
return { { actionType = StringDiff.ActionTypes.Insert, value = text2 } }
|
||||
end
|
||||
|
||||
if text2Length == 0 then
|
||||
-- It's simply deleting all of text1
|
||||
return { { actionType = StringDiff.ActionTypes.Delete, value = text1 } }
|
||||
end
|
||||
|
||||
local longText = if text1Length > text2Length then text1 else text2
|
||||
local shortText = if text1Length > text2Length then text2 else text1
|
||||
local shortTextLength = #shortText
|
||||
|
||||
-- Shortcut if the shorter string exists entirely inside the longer one
|
||||
local indexOf = if shortTextLength == 0 then nil else string.find(longText, shortText, 1, true)
|
||||
if indexOf ~= nil then
|
||||
local diffs = {
|
||||
{ actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, 1, indexOf - 1) },
|
||||
{ actionType = StringDiff.ActionTypes.Equal, value = shortText },
|
||||
{ actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, indexOf + shortTextLength) },
|
||||
}
|
||||
-- Swap insertions for deletions if diff is reversed
|
||||
if text1Length > text2Length then
|
||||
diffs[1].actionType, diffs[3].actionType = StringDiff.ActionTypes.Delete, StringDiff.ActionTypes.Delete
|
||||
end
|
||||
return diffs
|
||||
end
|
||||
|
||||
if shortTextLength == 1 then
|
||||
-- Single character string
|
||||
-- After the previous shortcut, the character can't be an equality
|
||||
return {
|
||||
{ actionType = StringDiff.ActionTypes.Delete, value = text1 },
|
||||
{ actionType = StringDiff.ActionTypes.Insert, value = text2 },
|
||||
}
|
||||
end
|
||||
|
||||
return StringDiff._bisect(text1, text2)
|
||||
end
|
||||
|
||||
function StringDiff._cleanupSemantic(diffs: Diffs): Diffs
|
||||
-- Reduce the number of edits by eliminating semantically trivial equalities.
|
||||
local changes = false
|
||||
local equalities = {} -- Stack of indices where equalities are found.
|
||||
local equalitiesLength = 0 -- Keeping our own length var is faster.
|
||||
local lastEquality: string? = nil
|
||||
-- Always equal to diffs[equalities[equalitiesLength]].value
|
||||
local pointer = 1 -- Index of current position.
|
||||
-- Number of characters that changed prior to the equality.
|
||||
local length_insertions1 = 0
|
||||
local length_deletions1 = 0
|
||||
-- Number of characters that changed after the equality.
|
||||
local length_insertions2 = 0
|
||||
local length_deletions2 = 0
|
||||
|
||||
while diffs[pointer] do
|
||||
if diffs[pointer].actionType == StringDiff.ActionTypes.Equal then -- Equality found.
|
||||
equalitiesLength = equalitiesLength + 1
|
||||
equalities[equalitiesLength] = pointer
|
||||
length_insertions1 = length_insertions2
|
||||
length_deletions1 = length_deletions2
|
||||
length_insertions2 = 0
|
||||
length_deletions2 = 0
|
||||
lastEquality = diffs[pointer].value
|
||||
else -- An insertion or deletion.
|
||||
if diffs[pointer].actionType == StringDiff.ActionTypes.Insert then
|
||||
length_insertions2 = length_insertions2 + #diffs[pointer].value
|
||||
else
|
||||
length_deletions2 = length_deletions2 + #diffs[pointer].value
|
||||
end
|
||||
-- Eliminate an equality that is smaller or equal to the edits on both
|
||||
-- sides of it.
|
||||
if
|
||||
lastEquality
|
||||
and (#lastEquality <= math.max(length_insertions1, length_deletions1))
|
||||
and (#lastEquality <= math.max(length_insertions2, length_deletions2))
|
||||
then
|
||||
-- Duplicate record.
|
||||
table.insert(
|
||||
diffs,
|
||||
equalities[equalitiesLength],
|
||||
{ actionType = StringDiff.ActionTypes.Delete, value = lastEquality }
|
||||
)
|
||||
-- Change second copy to insert.
|
||||
diffs[equalities[equalitiesLength] + 1].actionType = StringDiff.ActionTypes.Insert
|
||||
-- Throw away the equality we just deleted.
|
||||
equalitiesLength = equalitiesLength - 1
|
||||
-- Throw away the previous equality (it needs to be reevaluated).
|
||||
equalitiesLength = equalitiesLength - 1
|
||||
pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0
|
||||
length_insertions1, length_deletions1 = 0, 0 -- Reset the counters.
|
||||
length_insertions2, length_deletions2 = 0, 0
|
||||
lastEquality = nil
|
||||
changes = true
|
||||
end
|
||||
end
|
||||
pointer = pointer + 1
|
||||
end
|
||||
|
||||
-- Normalize the diff.
|
||||
if changes then
|
||||
StringDiff._reorderAndMerge(diffs)
|
||||
end
|
||||
StringDiff._cleanupSemanticLossless(diffs)
|
||||
|
||||
-- Find any overlaps between deletions and insertions.
|
||||
-- e.g: <del>abcxxx</del><ins>xxxdef</ins>
|
||||
-- -> <del>abc</del>xxx<ins>def</ins>
|
||||
-- e.g: <del>xxxabc</del><ins>defxxx</ins>
|
||||
-- -> <ins>def</ins>xxx<del>abc</del>
|
||||
-- Only extract an overlap if it is as big as the edit ahead or behind it.
|
||||
pointer = 2
|
||||
while diffs[pointer] do
|
||||
if
|
||||
diffs[pointer - 1].actionType == StringDiff.ActionTypes.Delete
|
||||
and diffs[pointer].actionType == StringDiff.ActionTypes.Insert
|
||||
then
|
||||
local deletion = diffs[pointer - 1].value
|
||||
local insertion = diffs[pointer].value
|
||||
local overlap_length1 = StringDiff._commonOverlap(deletion, insertion)
|
||||
local overlap_length2 = StringDiff._commonOverlap(insertion, deletion)
|
||||
if overlap_length1 >= overlap_length2 then
|
||||
if overlap_length1 >= #deletion / 2 or overlap_length1 >= #insertion / 2 then
|
||||
-- Overlap found. Insert an equality and trim the surrounding edits.
|
||||
table.insert(
|
||||
diffs,
|
||||
pointer,
|
||||
{ actionType = StringDiff.ActionTypes.Equal, value = string.sub(insertion, 1, overlap_length1) }
|
||||
)
|
||||
diffs[pointer - 1].value = string.sub(deletion, 1, #deletion - overlap_length1)
|
||||
diffs[pointer + 1].value = string.sub(insertion, overlap_length1 + 1)
|
||||
pointer = pointer + 1
|
||||
end
|
||||
else
|
||||
if overlap_length2 >= #deletion / 2 or overlap_length2 >= #insertion / 2 then
|
||||
-- Reverse overlap found.
|
||||
-- Insert an equality and swap and trim the surrounding edits.
|
||||
table.insert(
|
||||
diffs,
|
||||
pointer,
|
||||
{ actionType = StringDiff.ActionTypes.Equal, value = string.sub(deletion, 1, overlap_length2) }
|
||||
)
|
||||
diffs[pointer - 1] = {
|
||||
actionType = StringDiff.ActionTypes.Insert,
|
||||
value = string.sub(insertion, 1, #insertion - overlap_length2),
|
||||
}
|
||||
diffs[pointer + 1] = {
|
||||
actionType = StringDiff.ActionTypes.Delete,
|
||||
value = string.sub(deletion, overlap_length2 + 1),
|
||||
}
|
||||
pointer = pointer + 1
|
||||
end
|
||||
end
|
||||
pointer = pointer + 1
|
||||
end
|
||||
pointer = pointer + 1
|
||||
end
|
||||
|
||||
return diffs
|
||||
end
|
||||
|
||||
@@ -124,51 +304,164 @@ function StringDiff._sharedSuffix(text1: string, text2: string): number
|
||||
return pointerMid
|
||||
end
|
||||
|
||||
function StringDiff._computeDiff(text1: string, text2: string): Diffs
|
||||
-- Assumes that the prefix and suffix have already been trimmed off
|
||||
-- and shortcut returns have been made so these texts must be different
|
||||
function StringDiff._commonOverlap(text1: string, text2: string): number
|
||||
-- Determine if the suffix of one string is the prefix of another.
|
||||
|
||||
local text1Length, text2Length = #text1, #text2
|
||||
|
||||
if text1Length == 0 then
|
||||
-- It's simply inserting all of text2 into text1
|
||||
return { { actionType = StringDiff.ActionTypes.Insert, value = text2 } }
|
||||
-- Cache the text lengths to prevent multiple calls.
|
||||
local text1_length = #text1
|
||||
local text2_length = #text2
|
||||
-- Eliminate the null case.
|
||||
if text1_length == 0 or text2_length == 0 then
|
||||
return 0
|
||||
end
|
||||
-- Truncate the longer string.
|
||||
if text1_length > text2_length then
|
||||
text1 = string.sub(text1, text1_length - text2_length + 1)
|
||||
elseif text1_length < text2_length then
|
||||
text2 = string.sub(text2, 1, text1_length)
|
||||
end
|
||||
local text_length = math.min(text1_length, text2_length)
|
||||
-- Quick check for the worst case.
|
||||
if text1 == text2 then
|
||||
return text_length
|
||||
end
|
||||
|
||||
if text2Length == 0 then
|
||||
-- It's simply deleting all of text1
|
||||
return { { actionType = StringDiff.ActionTypes.Delete, value = text1 } }
|
||||
-- Start by looking for a single character match
|
||||
-- and increase length until no match is found.
|
||||
-- Performance analysis: https://neil.fraser.name/news/2010/11/04/
|
||||
local best = 0
|
||||
local length = 1
|
||||
while true do
|
||||
local pattern = string.sub(text1, text_length - length + 1)
|
||||
local found = string.find(text2, pattern, 1, true)
|
||||
if found == nil then
|
||||
return best
|
||||
end
|
||||
length = length + found - 1
|
||||
if found == 1 or string.sub(text1, text_length - length + 1) == string.sub(text2, 1, length) then
|
||||
best = length
|
||||
length = length + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function StringDiff._cleanupSemanticScore(one: string, two: string): number
|
||||
-- Given two strings, compute a score representing whether the internal
|
||||
-- boundary falls on logical boundaries.
|
||||
-- Scores range from 6 (best) to 0 (worst).
|
||||
|
||||
if (#one == 0) or (#two == 0) then
|
||||
-- Edges are the best.
|
||||
return 6
|
||||
end
|
||||
|
||||
local longText = if text1Length > text2Length then text1 else text2
|
||||
local shortText = if text1Length > text2Length then text2 else text1
|
||||
local shortTextLength = #shortText
|
||||
-- Each port of this function behaves slightly differently due to
|
||||
-- subtle differences in each language's definition of things like
|
||||
-- 'whitespace'. Since this function's purpose is largely cosmetic,
|
||||
-- the choice has been made to use each language's native features
|
||||
-- rather than force total conformity.
|
||||
local char1 = string.sub(one, -1)
|
||||
local char2 = string.sub(two, 1, 1)
|
||||
local nonAlphaNumeric1 = string.match(char1, "%W")
|
||||
local nonAlphaNumeric2 = string.match(char2, "%W")
|
||||
local whitespace1 = nonAlphaNumeric1 and string.match(char1, "%s")
|
||||
local whitespace2 = nonAlphaNumeric2 and string.match(char2, "%s")
|
||||
local lineBreak1 = whitespace1 and string.match(char1, "%c")
|
||||
local lineBreak2 = whitespace2 and string.match(char2, "%c")
|
||||
local blankLine1 = lineBreak1 and string.match(one, "\n\r?\n$")
|
||||
local blankLine2 = lineBreak2 and string.match(two, "^\r?\n\r?\n")
|
||||
|
||||
-- Shortcut if the shorter string exists entirely inside the longer one
|
||||
local indexOf = if shortTextLength == 0 then nil else string.find(longText, shortText, 1, true)
|
||||
if indexOf ~= nil then
|
||||
local diffs = {
|
||||
{ actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, 1, indexOf - 1) },
|
||||
{ actionType = StringDiff.ActionTypes.Equal, value = shortText },
|
||||
{ actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, indexOf + shortTextLength) },
|
||||
}
|
||||
-- Swap insertions for deletions if diff is reversed
|
||||
if text1Length > text2Length then
|
||||
diffs[1].actionType, diffs[3].actionType = StringDiff.ActionTypes.Delete, StringDiff.ActionTypes.Delete
|
||||
if blankLine1 or blankLine2 then
|
||||
-- Five points for blank lines.
|
||||
return 5
|
||||
elseif lineBreak1 or lineBreak2 then
|
||||
-- Four points for line breaks
|
||||
-- DEVIATION: Prefer to start on a line break instead of end on it
|
||||
return if lineBreak1 then 4 else 4.5
|
||||
elseif nonAlphaNumeric1 and not whitespace1 and whitespace2 then
|
||||
-- Three points for end of sentences.
|
||||
return 3
|
||||
elseif whitespace1 or whitespace2 then
|
||||
-- Two points for whitespace.
|
||||
return 2
|
||||
elseif nonAlphaNumeric1 or nonAlphaNumeric2 then
|
||||
-- One point for non-alphanumeric.
|
||||
return 1
|
||||
end
|
||||
return diffs
|
||||
return 0
|
||||
end
|
||||
|
||||
function StringDiff._cleanupSemanticLossless(diffs: Diffs)
|
||||
-- Look for single edits surrounded on both sides by equalities
|
||||
-- which can be shifted sideways to align the edit to a word boundary.
|
||||
-- e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
|
||||
|
||||
local pointer = 2
|
||||
-- Intentionally ignore the first and last element (don't need checking).
|
||||
while diffs[pointer + 1] do
|
||||
local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1]
|
||||
if
|
||||
(prevDiff.actionType == StringDiff.ActionTypes.Equal)
|
||||
and (nextDiff.actionType == StringDiff.ActionTypes.Equal)
|
||||
then
|
||||
-- This is a single edit surrounded by equalities.
|
||||
local diff = diffs[pointer]
|
||||
|
||||
local equality1 = prevDiff.value
|
||||
local edit = diff.value
|
||||
local equality2 = nextDiff.value
|
||||
|
||||
-- First, shift the edit as far left as possible.
|
||||
local commonOffset = StringDiff._sharedSuffix(equality1, edit)
|
||||
if commonOffset > 0 then
|
||||
local commonString = string.sub(edit, -commonOffset)
|
||||
equality1 = string.sub(equality1, 1, -commonOffset - 1)
|
||||
edit = commonString .. string.sub(edit, 1, -commonOffset - 1)
|
||||
equality2 = commonString .. equality2
|
||||
end
|
||||
|
||||
if shortTextLength == 1 then
|
||||
-- Single character string
|
||||
-- After the previous shortcut, the character can't be an equality
|
||||
return {
|
||||
{ actionType = StringDiff.ActionTypes.Delete, value = text1 },
|
||||
{ actionType = StringDiff.ActionTypes.Insert, value = text2 },
|
||||
}
|
||||
end
|
||||
-- Second, step character by character right, looking for the best fit.
|
||||
local bestEquality1 = equality1
|
||||
local bestEdit = edit
|
||||
local bestEquality2 = equality2
|
||||
local bestScore = StringDiff._cleanupSemanticScore(equality1, edit)
|
||||
+ StringDiff._cleanupSemanticScore(edit, equality2)
|
||||
|
||||
return StringDiff._bisect(text1, text2)
|
||||
while string.byte(edit, 1) == string.byte(equality2, 1) do
|
||||
equality1 = equality1 .. string.sub(edit, 1, 1)
|
||||
edit = string.sub(edit, 2) .. string.sub(equality2, 1, 1)
|
||||
equality2 = string.sub(equality2, 2)
|
||||
local score = StringDiff._cleanupSemanticScore(equality1, edit)
|
||||
+ StringDiff._cleanupSemanticScore(edit, equality2)
|
||||
-- The > (rather than >=) encourages leading rather than trailing whitespace on edits.
|
||||
-- I just think it looks better for indentation changes to start the line,
|
||||
-- since then indenting several lines all have aligned diffs at the start
|
||||
if score > bestScore then
|
||||
bestScore = score
|
||||
bestEquality1 = equality1
|
||||
bestEdit = edit
|
||||
bestEquality2 = equality2
|
||||
end
|
||||
end
|
||||
if prevDiff.value ~= bestEquality1 then
|
||||
-- We have an improvement, save it back to the diff.
|
||||
if #bestEquality1 > 0 then
|
||||
diffs[pointer - 1].value = bestEquality1
|
||||
else
|
||||
table.remove(diffs, pointer - 1)
|
||||
pointer = pointer - 1
|
||||
end
|
||||
diffs[pointer].value = bestEdit
|
||||
if #bestEquality2 > 0 then
|
||||
diffs[pointer + 1].value = bestEquality2
|
||||
else
|
||||
table.remove(diffs, pointer + 1)
|
||||
pointer = pointer - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
pointer = pointer + 1
|
||||
end
|
||||
end
|
||||
|
||||
function StringDiff._bisect(text1: string, text2: string): Diffs
|
||||
|
||||
@@ -5,15 +5,15 @@ local Packages = Rojo.Packages
|
||||
local Roact = require(Packages.Roact)
|
||||
local Log = require(Packages.Log)
|
||||
local Highlighter = require(Packages.Highlighter)
|
||||
Highlighter.matchStudioSettings()
|
||||
local StringDiff = require(script:FindFirstChild("StringDiff"))
|
||||
|
||||
local Timer = require(Plugin.Timer)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
|
||||
|
||||
local CodeLabel = require(Plugin.App.Components.CodeLabel)
|
||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
|
||||
|
||||
local e = Roact.createElement
|
||||
|
||||
@@ -21,26 +21,29 @@ local StringDiffVisualizer = Roact.Component:extend("StringDiffVisualizer")
|
||||
|
||||
function StringDiffVisualizer:init()
|
||||
self.scriptBackground, self.setScriptBackground = Roact.createBinding(Color3.fromRGB(0, 0, 0))
|
||||
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
self.updateEvent = Instance.new("BindableEvent")
|
||||
self.lineHeight, self.setLineHeight = Roact.createBinding(15)
|
||||
self.canvasPosition, self.setCanvasPosition = Roact.createBinding(Vector2.zero)
|
||||
self.windowWidth, self.setWindowWidth = Roact.createBinding(math.huge)
|
||||
|
||||
-- Ensure that the script background is up to date with the current theme
|
||||
self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function()
|
||||
task.defer(function()
|
||||
-- Defer to allow Highlighter to process the theme change first
|
||||
-- Delay to allow Highlighter to process the theme change first
|
||||
task.delay(1 / 20, function()
|
||||
self:updateScriptBackground()
|
||||
self:updateDiffs()
|
||||
-- Rerender the virtual list elements
|
||||
self.updateEvent:Fire()
|
||||
end)
|
||||
end)
|
||||
|
||||
self:updateScriptBackground()
|
||||
|
||||
self:setState({
|
||||
add = {},
|
||||
remove = {},
|
||||
})
|
||||
self:updateDiffs()
|
||||
end
|
||||
|
||||
function StringDiffVisualizer:willUnmount()
|
||||
self.themeChangedConnection:Disconnect()
|
||||
self.updateEvent:Destroy()
|
||||
end
|
||||
|
||||
function StringDiffVisualizer:updateScriptBackground()
|
||||
@@ -51,96 +54,188 @@ function StringDiffVisualizer:updateScriptBackground()
|
||||
end
|
||||
|
||||
function StringDiffVisualizer:didUpdate(previousProps)
|
||||
if previousProps.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then
|
||||
local add, remove = self:calculateDiffLines()
|
||||
self:setState({
|
||||
add = add,
|
||||
remove = remove,
|
||||
})
|
||||
if
|
||||
previousProps.currentString ~= self.props.currentString
|
||||
or previousProps.incomingString ~= self.props.incomingString
|
||||
then
|
||||
self:updateDiffs()
|
||||
end
|
||||
end
|
||||
|
||||
function StringDiffVisualizer:calculateContentSize(theme)
|
||||
local oldString, newString = self.props.oldString, self.props.newString
|
||||
|
||||
local oldStringBounds = getTextBoundsAsync(oldString, theme.Font.Code, theme.TextSize.Code, math.huge)
|
||||
local newStringBounds = getTextBoundsAsync(newString, theme.Font.Code, theme.TextSize.Code, math.huge)
|
||||
|
||||
self.setContentSize(
|
||||
Vector2.new(math.max(oldStringBounds.X, newStringBounds.X), math.max(oldStringBounds.Y, newStringBounds.Y))
|
||||
)
|
||||
end
|
||||
|
||||
function StringDiffVisualizer:calculateDiffLines()
|
||||
Timer.start("StringDiffVisualizer:calculateDiffLines")
|
||||
local oldString, newString = self.props.oldString, self.props.newString
|
||||
function StringDiffVisualizer:updateDiffs()
|
||||
Timer.start("StringDiffVisualizer:updateDiffs")
|
||||
local currentString, incomingString = self.props.currentString, self.props.incomingString
|
||||
|
||||
-- Diff the two texts
|
||||
local startClock = os.clock()
|
||||
local diffs = StringDiff.findDiffs(oldString, newString)
|
||||
local diffs =
|
||||
StringDiff.findDiffs((string.gsub(currentString, "\t", " ")), (string.gsub(incomingString, "\t", " ")))
|
||||
local stopClock = os.clock()
|
||||
|
||||
Log.trace(
|
||||
"Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections",
|
||||
#oldString,
|
||||
#newString,
|
||||
#currentString,
|
||||
#incomingString,
|
||||
math.round((stopClock - startClock) * 1000 * 1000),
|
||||
#diffs
|
||||
)
|
||||
|
||||
-- Determine which lines to highlight
|
||||
local add, remove = {}, {}
|
||||
-- Build the rich text lines
|
||||
local currentRichTextLines = Highlighter.buildRichTextLines({
|
||||
src = currentString,
|
||||
})
|
||||
local incomingRichTextLines = Highlighter.buildRichTextLines({
|
||||
src = incomingString,
|
||||
})
|
||||
|
||||
local oldLineNum, newLineNum = 1, 1
|
||||
local maxLines = math.max(#currentRichTextLines, #incomingRichTextLines)
|
||||
|
||||
-- Find the diff locations
|
||||
local currentDiffs, incomingDiffs = {}, {}
|
||||
local firstDiffLineNum = 0
|
||||
|
||||
local currentLineNum, incomingLineNum = 1, 1
|
||||
local currentIdx, incomingIdx = 1, 1
|
||||
for _, diff in diffs do
|
||||
local actionType, text = diff.actionType, diff.value
|
||||
local lines = select(2, string.gsub(text, "\n", "\n"))
|
||||
local lineCount = select(2, string.gsub(text, "\n", "\n"))
|
||||
local lines = string.split(text, "\n")
|
||||
|
||||
if actionType == StringDiff.ActionTypes.Equal then
|
||||
oldLineNum += lines
|
||||
newLineNum += lines
|
||||
elseif actionType == StringDiff.ActionTypes.Insert then
|
||||
if lines > 0 then
|
||||
local textLines = string.split(text, "\n")
|
||||
for i, textLine in textLines do
|
||||
if string.match(textLine, "%S") then
|
||||
add[newLineNum + i - 1] = true
|
||||
end
|
||||
end
|
||||
if lineCount > 0 then
|
||||
-- Jump cursor ahead to last line
|
||||
currentLineNum += lineCount
|
||||
incomingLineNum += lineCount
|
||||
currentIdx = #lines[#lines]
|
||||
incomingIdx = #lines[#lines]
|
||||
else
|
||||
if string.match(text, "%S") then
|
||||
add[newLineNum] = true
|
||||
-- Move along this line
|
||||
currentIdx += #text
|
||||
incomingIdx += #text
|
||||
end
|
||||
|
||||
continue
|
||||
end
|
||||
|
||||
if actionType == StringDiff.ActionTypes.Insert then
|
||||
if firstDiffLineNum == 0 then
|
||||
firstDiffLineNum = incomingLineNum
|
||||
end
|
||||
|
||||
for i, lineText in lines do
|
||||
if i > 1 then
|
||||
-- Move to next line
|
||||
incomingLineNum += 1
|
||||
incomingIdx = 0
|
||||
end
|
||||
if not incomingDiffs[incomingLineNum] then
|
||||
incomingDiffs[incomingLineNum] = {}
|
||||
end
|
||||
-- Mark these characters on this line
|
||||
table.insert(incomingDiffs[incomingLineNum], {
|
||||
start = incomingIdx,
|
||||
stop = incomingIdx + #lineText,
|
||||
})
|
||||
incomingIdx += #lineText
|
||||
end
|
||||
newLineNum += lines
|
||||
elseif actionType == StringDiff.ActionTypes.Delete then
|
||||
if lines > 0 then
|
||||
local textLines = string.split(text, "\n")
|
||||
for i, textLine in textLines do
|
||||
if string.match(textLine, "%S") then
|
||||
remove[oldLineNum + i - 1] = true
|
||||
if firstDiffLineNum == 0 then
|
||||
firstDiffLineNum = currentLineNum
|
||||
end
|
||||
|
||||
for i, lineText in lines do
|
||||
if i > 1 then
|
||||
-- Move to next line
|
||||
currentLineNum += 1
|
||||
currentIdx = 0
|
||||
end
|
||||
else
|
||||
if string.match(text, "%S") then
|
||||
remove[oldLineNum] = true
|
||||
if not currentDiffs[currentLineNum] then
|
||||
currentDiffs[currentLineNum] = {}
|
||||
end
|
||||
-- Mark these characters on this line
|
||||
table.insert(currentDiffs[currentLineNum], {
|
||||
start = currentIdx,
|
||||
stop = currentIdx + #lineText,
|
||||
})
|
||||
currentIdx += #lineText
|
||||
end
|
||||
oldLineNum += lines
|
||||
else
|
||||
Log.warn("Unknown diff action: {} {}", actionType, text)
|
||||
end
|
||||
end
|
||||
|
||||
Timer.stop()
|
||||
return add, remove
|
||||
|
||||
self:setState({
|
||||
maxLines = maxLines,
|
||||
currentRichTextLines = currentRichTextLines,
|
||||
incomingRichTextLines = incomingRichTextLines,
|
||||
currentDiffs = currentDiffs,
|
||||
incomingDiffs = incomingDiffs,
|
||||
})
|
||||
|
||||
-- Scroll to the first diff line
|
||||
task.defer(self.setCanvasPosition, Vector2.new(0, math.max(0, (firstDiffLineNum - 4) * 16)))
|
||||
end
|
||||
|
||||
function StringDiffVisualizer:render()
|
||||
local oldString, newString = self.props.oldString, self.props.newString
|
||||
local currentDiffs, incomingDiffs = self.state.currentDiffs, self.state.incomingDiffs
|
||||
local currentRichTextLines, incomingRichTextLines =
|
||||
self.state.currentRichTextLines, self.state.incomingRichTextLines
|
||||
local maxLines = self.state.maxLines
|
||||
|
||||
return Theme.with(function(theme)
|
||||
self:calculateContentSize(theme)
|
||||
self.setLineHeight(theme.TextSize.Code)
|
||||
|
||||
-- Calculate the width of the canvas
|
||||
-- (One line at a time to avoid the char limit of getTextBoundsAsync)
|
||||
local canvasWidth = 0
|
||||
for i = 1, maxLines do
|
||||
local currentLine = currentRichTextLines[i]
|
||||
if currentLine and string.find(currentLine, "%S") then
|
||||
local bounds = getTextBoundsAsync(currentLine, theme.Font.Code, theme.TextSize.Code, math.huge, true)
|
||||
if bounds.X > canvasWidth then
|
||||
canvasWidth = bounds.X
|
||||
end
|
||||
end
|
||||
local incomingLine = incomingRichTextLines[i]
|
||||
if incomingLine and string.find(incomingLine, "%S") then
|
||||
local bounds = getTextBoundsAsync(incomingLine, theme.Font.Code, theme.TextSize.Code, math.huge, true)
|
||||
if bounds.X > canvasWidth then
|
||||
canvasWidth = bounds.X
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local lineNumberWidth =
|
||||
getTextBoundsAsync(tostring(maxLines), theme.Font.Code, theme.TextSize.Body, math.huge, true).X
|
||||
|
||||
canvasWidth += lineNumberWidth + 12
|
||||
|
||||
local removalScrollMarkers = {}
|
||||
local insertionScrollMarkers = {}
|
||||
for lineNum in currentDiffs do
|
||||
table.insert(
|
||||
removalScrollMarkers,
|
||||
e("Frame", {
|
||||
Size = UDim2.fromScale(0.5, 1 / maxLines),
|
||||
Position = UDim2.fromScale(0, (lineNum - 1) / maxLines),
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = theme.Diff.Background.Remove,
|
||||
})
|
||||
)
|
||||
end
|
||||
for lineNum in incomingDiffs do
|
||||
table.insert(
|
||||
insertionScrollMarkers,
|
||||
e("Frame", {
|
||||
Size = UDim2.fromScale(0.5, 1 / maxLines),
|
||||
Position = UDim2.fromScale(0.5, (lineNum - 1) / maxLines),
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = theme.Diff.Background.Add,
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
return e(BorderedContainer, {
|
||||
size = self.props.size,
|
||||
@@ -159,6 +254,14 @@ function StringDiffVisualizer:render()
|
||||
CornerRadius = UDim.new(0, 5),
|
||||
}),
|
||||
}),
|
||||
Main = e("Frame", {
|
||||
Size = UDim2.new(1, -10, 1, -2),
|
||||
Position = UDim2.new(0, 2, 0, 2),
|
||||
BackgroundTransparency = 1,
|
||||
[Roact.Change.AbsoluteSize] = function(rbx)
|
||||
self.setWindowWidth(rbx.AbsoluteSize.X * 0.5 - 10)
|
||||
end,
|
||||
}, {
|
||||
Separator = e("Frame", {
|
||||
Size = UDim2.new(0, 2, 1, 0),
|
||||
Position = UDim2.new(0.5, 0, 0, 0),
|
||||
@@ -167,35 +270,180 @@ function StringDiffVisualizer:render()
|
||||
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||
BackgroundTransparency = 0.5,
|
||||
}),
|
||||
Old = e(ScrollingFrame, {
|
||||
position = UDim2.new(0, 2, 0, 2),
|
||||
size = UDim2.new(0.5, -7, 1, -4),
|
||||
scrollingDirection = Enum.ScrollingDirection.XY,
|
||||
transparency = self.props.transparency,
|
||||
contentSize = self.contentSize,
|
||||
}, {
|
||||
Source = e(CodeLabel, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
Current = e(VirtualScroller, {
|
||||
position = UDim2.new(0, 0, 0, 0),
|
||||
text = oldString,
|
||||
lineBackground = theme.Diff.Background.Remove,
|
||||
markedLines = self.state.remove,
|
||||
}),
|
||||
}),
|
||||
New = e(ScrollingFrame, {
|
||||
position = UDim2.new(0.5, 5, 0, 2),
|
||||
size = UDim2.new(0.5, -7, 1, -4),
|
||||
scrollingDirection = Enum.ScrollingDirection.XY,
|
||||
size = UDim2.new(0.5, -1, 1, 0),
|
||||
transparency = self.props.transparency,
|
||||
contentSize = self.contentSize,
|
||||
count = maxLines,
|
||||
updateEvent = self.updateEvent.Event,
|
||||
canvasWidth = canvasWidth,
|
||||
canvasPosition = self.canvasPosition,
|
||||
onCanvasPositionChanged = self.setCanvasPosition,
|
||||
render = function(i)
|
||||
local lineDiffs = currentDiffs[i]
|
||||
local diffFrames = table.create(if lineDiffs then #lineDiffs else 0)
|
||||
|
||||
-- Show diff markers over the specific changed characters
|
||||
if lineDiffs then
|
||||
local charWidth = math.round(theme.TextSize.Code * 0.5)
|
||||
for diffIdx, diff in lineDiffs do
|
||||
local start, stop = diff.start, diff.stop
|
||||
diffFrames[diffIdx] = e("Frame", {
|
||||
Size = if #lineDiffs == 1
|
||||
and start == 0
|
||||
and stop == 0
|
||||
then UDim2.fromScale(1, 1)
|
||||
else UDim2.new(
|
||||
0,
|
||||
math.max(charWidth * (stop - start), charWidth * 0.4),
|
||||
1,
|
||||
0
|
||||
),
|
||||
Position = UDim2.fromOffset(charWidth * start, 0),
|
||||
BackgroundColor3 = theme.Diff.Background.Remove,
|
||||
BackgroundTransparency = 0.85,
|
||||
BorderSizePixel = 0,
|
||||
ZIndex = -1,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return Roact.createFragment({
|
||||
LineNumber = e("TextLabel", {
|
||||
Size = UDim2.new(0, lineNumberWidth + 8, 1, 0),
|
||||
Text = i,
|
||||
BackgroundColor3 = Color3.new(0, 0, 0),
|
||||
BackgroundTransparency = 0.9,
|
||||
BorderSizePixel = 0,
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = if lineDiffs then theme.Diff.Background.Remove else theme.SubTextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Right,
|
||||
}, {
|
||||
Source = e(CodeLabel, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
position = UDim2.new(0, 0, 0, 0),
|
||||
text = newString,
|
||||
lineBackground = theme.Diff.Background.Add,
|
||||
markedLines = self.state.add,
|
||||
Padding = e("UIPadding", { PaddingRight = UDim.new(0, 6) }),
|
||||
}),
|
||||
Content = e("Frame", {
|
||||
Size = UDim2.new(1, -(lineNumberWidth + 10), 1, 0),
|
||||
Position = UDim2.fromScale(1, 0),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
BackgroundColor3 = theme.Diff.Background.Remove,
|
||||
BackgroundTransparency = if lineDiffs then 0.95 else 1,
|
||||
BorderSizePixel = 0,
|
||||
}, {
|
||||
CodeLabel = e("TextLabel", {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
Position = UDim2.fromScale(0, 0),
|
||||
Text = currentRichTextLines[i] or "",
|
||||
RichText = true,
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Code,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
TextColor3 = Color3.fromRGB(255, 255, 255),
|
||||
}),
|
||||
DiffFrames = Roact.createFragment(diffFrames),
|
||||
}),
|
||||
})
|
||||
end,
|
||||
getHeightBinding = function()
|
||||
return self.lineHeight
|
||||
end,
|
||||
}),
|
||||
Incoming = e(VirtualScroller, {
|
||||
position = UDim2.new(0.5, 1, 0, 0),
|
||||
size = UDim2.new(0.5, -1, 1, 0),
|
||||
transparency = self.props.transparency,
|
||||
count = maxLines,
|
||||
updateEvent = self.updateEvent.Event,
|
||||
canvasWidth = canvasWidth,
|
||||
canvasPosition = self.canvasPosition,
|
||||
onCanvasPositionChanged = self.setCanvasPosition,
|
||||
render = function(i)
|
||||
local lineDiffs = incomingDiffs[i]
|
||||
local diffFrames = table.create(if lineDiffs then #lineDiffs else 0)
|
||||
|
||||
-- Show diff markers over the specific changed characters
|
||||
if lineDiffs then
|
||||
local charWidth = math.round(theme.TextSize.Code * 0.5)
|
||||
for diffIdx, diff in lineDiffs do
|
||||
local start, stop = diff.start, diff.stop
|
||||
diffFrames[diffIdx] = e("Frame", {
|
||||
Size = if #lineDiffs == 1
|
||||
and start == 0
|
||||
and stop == 0
|
||||
then UDim2.fromScale(1, 1)
|
||||
else UDim2.new(
|
||||
0,
|
||||
math.max(charWidth * (stop - start), charWidth * 0.4),
|
||||
1,
|
||||
0
|
||||
),
|
||||
Position = UDim2.fromOffset(charWidth * start, 0),
|
||||
BackgroundColor3 = theme.Diff.Background.Add,
|
||||
BackgroundTransparency = 0.85,
|
||||
BorderSizePixel = 0,
|
||||
ZIndex = -1,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return Roact.createFragment({
|
||||
LineNumber = e("TextLabel", {
|
||||
Size = UDim2.new(0, lineNumberWidth + 8, 1, 0),
|
||||
Text = i,
|
||||
BackgroundColor3 = Color3.new(0, 0, 0),
|
||||
BackgroundTransparency = 0.9,
|
||||
BorderSizePixel = 0,
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Body,
|
||||
TextColor3 = if lineDiffs then theme.Diff.Background.Add else theme.SubTextColor,
|
||||
TextXAlignment = Enum.TextXAlignment.Right,
|
||||
}, {
|
||||
Padding = e("UIPadding", { PaddingRight = UDim.new(0, 6) }),
|
||||
}),
|
||||
Content = e("Frame", {
|
||||
Size = UDim2.new(1, -(lineNumberWidth + 10), 1, 0),
|
||||
Position = UDim2.fromScale(1, 0),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
BackgroundColor3 = theme.Diff.Background.Add,
|
||||
BackgroundTransparency = if lineDiffs then 0.95 else 1,
|
||||
BorderSizePixel = 0,
|
||||
}, {
|
||||
CodeLabel = e("TextLabel", {
|
||||
Size = UDim2.fromScale(1, 1),
|
||||
Position = UDim2.fromScale(0, 0),
|
||||
Text = incomingRichTextLines[i] or "",
|
||||
RichText = true,
|
||||
BackgroundColor3 = theme.Diff.Background.Add,
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
FontFace = theme.Font.Code,
|
||||
TextSize = theme.TextSize.Code,
|
||||
TextXAlignment = Enum.TextXAlignment.Left,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
TextColor3 = Color3.fromRGB(255, 255, 255),
|
||||
}),
|
||||
DiffFrames = Roact.createFragment(diffFrames),
|
||||
}),
|
||||
})
|
||||
end,
|
||||
getHeightBinding = function()
|
||||
return self.lineHeight
|
||||
end,
|
||||
}),
|
||||
}),
|
||||
ScrollMarkers = e("Frame", {
|
||||
Size = self.windowWidth:map(function(windowWidth)
|
||||
return UDim2.new(0, 8, 1, -4 - (if canvasWidth > windowWidth then 10 else 0))
|
||||
end),
|
||||
Position = UDim2.new(1, -2, 0, 2),
|
||||
AnchorPoint = Vector2.new(1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
insertions = Roact.createFragment(insertionScrollMarkers),
|
||||
removals = Roact.createFragment(removalScrollMarkers),
|
||||
}),
|
||||
})
|
||||
end)
|
||||
|
||||
@@ -15,8 +15,10 @@ local VirtualScroller = Roact.Component:extend("VirtualScroller")
|
||||
function VirtualScroller:init()
|
||||
self.scrollFrameRef = Roact.createRef()
|
||||
self:setState({
|
||||
WindowSize = Vector2.new(),
|
||||
CanvasPosition = Vector2.new(),
|
||||
WindowSize = Vector2.zero,
|
||||
CanvasPosition = if self.props.canvasPosition
|
||||
then self.props.canvasPosition:getValue() or Vector2.zero
|
||||
else Vector2.zero,
|
||||
})
|
||||
|
||||
self.totalCanvas, self.setTotalCanvas = Roact.createBinding(0)
|
||||
@@ -41,6 +43,10 @@ function VirtualScroller:didMount()
|
||||
|
||||
local canvasPositionSignal = rbx:GetPropertyChangedSignal("CanvasPosition")
|
||||
self.canvasPositionChanged = canvasPositionSignal:Connect(function()
|
||||
if self.props.onCanvasPositionChanged then
|
||||
pcall(self.props.onCanvasPositionChanged, rbx.CanvasPosition)
|
||||
end
|
||||
|
||||
if math.abs(rbx.CanvasPosition.Y - self.state.CanvasPosition.Y) > 5 then
|
||||
self:setState({ CanvasPosition = rbx.CanvasPosition })
|
||||
self:refresh()
|
||||
@@ -134,8 +140,9 @@ function VirtualScroller:render()
|
||||
BackgroundColor3 = props.backgroundColor3 or theme.BorderedContainer.BackgroundColor,
|
||||
BorderColor3 = props.borderColor3 or theme.BorderedContainer.BorderColor,
|
||||
CanvasSize = self.totalCanvas:map(function(s)
|
||||
return UDim2.fromOffset(0, s)
|
||||
return UDim2.fromOffset(props.canvasWidth or 0, s)
|
||||
end),
|
||||
CanvasPosition = self.props.canvasPosition,
|
||||
ScrollBarThickness = 9,
|
||||
ScrollBarImageColor3 = theme.ScrollBarColor,
|
||||
ScrollBarImageTransparency = props.transparency:map(function(value)
|
||||
@@ -146,7 +153,7 @@ function VirtualScroller:render()
|
||||
BottomImage = Assets.Images.ScrollBar.Bottom,
|
||||
|
||||
ElasticBehavior = Enum.ElasticBehavior.Always,
|
||||
ScrollingDirection = Enum.ScrollingDirection.Y,
|
||||
ScrollingDirection = Enum.ScrollingDirection.XY,
|
||||
VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
|
||||
[Roact.Ref] = self.scrollFrameRef,
|
||||
}, {
|
||||
|
||||
@@ -4,8 +4,6 @@ local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Timer = require(Plugin.Timer)
|
||||
local PatchTree = require(Plugin.PatchTree)
|
||||
local Settings = require(Plugin.Settings)
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
local TextButton = require(Plugin.App.Components.TextButton)
|
||||
@@ -24,36 +22,13 @@ function ConfirmingPage:init()
|
||||
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
||||
|
||||
self:setState({
|
||||
patchTree = nil,
|
||||
showingStringDiff = false,
|
||||
oldString = "",
|
||||
newString = "",
|
||||
currentString = "",
|
||||
incomingString = "",
|
||||
showingTableDiff = false,
|
||||
oldTable = {},
|
||||
newTable = {},
|
||||
})
|
||||
|
||||
if self.props.confirmData and self.props.confirmData.patch and self.props.confirmData.instanceMap then
|
||||
self:buildPatchTree()
|
||||
end
|
||||
end
|
||||
|
||||
function ConfirmingPage:didUpdate(prevProps)
|
||||
if prevProps.confirmData ~= self.props.confirmData then
|
||||
self:buildPatchTree()
|
||||
end
|
||||
end
|
||||
|
||||
function ConfirmingPage:buildPatchTree()
|
||||
Timer.start("ConfirmingPage:buildPatchTree")
|
||||
self:setState({
|
||||
patchTree = PatchTree.build(
|
||||
self.props.confirmData.patch,
|
||||
self.props.confirmData.instanceMap,
|
||||
{ "Property", "Current", "Incoming" }
|
||||
),
|
||||
})
|
||||
Timer.stop()
|
||||
end
|
||||
|
||||
function ConfirmingPage:render()
|
||||
@@ -79,13 +54,13 @@ function ConfirmingPage:render()
|
||||
transparency = self.props.transparency,
|
||||
layoutOrder = 3,
|
||||
|
||||
patchTree = self.state.patchTree,
|
||||
patchTree = self.props.patchTree,
|
||||
|
||||
showStringDiff = function(oldString: string, newString: string)
|
||||
showStringDiff = function(currentString: string, incomingString: string)
|
||||
self:setState({
|
||||
showingStringDiff = true,
|
||||
oldString = oldString,
|
||||
newString = newString,
|
||||
currentString = currentString,
|
||||
incomingString = incomingString,
|
||||
})
|
||||
end,
|
||||
showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
|
||||
@@ -192,8 +167,8 @@ function ConfirmingPage:render()
|
||||
anchorPoint = Vector2.new(0, 0),
|
||||
transparency = self.props.transparency,
|
||||
|
||||
oldString = self.state.oldString,
|
||||
newString = self.state.newString,
|
||||
currentString = self.state.currentString,
|
||||
incomingString = self.state.incomingString,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -307,8 +307,8 @@ function ConnectedPage:init()
|
||||
renderChanges = false,
|
||||
hoveringChangeInfo = false,
|
||||
showingStringDiff = false,
|
||||
oldString = "",
|
||||
newString = "",
|
||||
currentString = "",
|
||||
incomingString = "",
|
||||
})
|
||||
|
||||
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
|
||||
@@ -511,11 +511,11 @@ function ConnectedPage:render()
|
||||
patchData = self.props.patchData,
|
||||
patchTree = self.props.patchTree,
|
||||
serveSession = self.props.serveSession,
|
||||
showStringDiff = function(oldString: string, newString: string)
|
||||
showStringDiff = function(currentString: string, incomingString: string)
|
||||
self:setState({
|
||||
showingStringDiff = true,
|
||||
oldString = oldString,
|
||||
newString = newString,
|
||||
currentString = currentString,
|
||||
incomingString = incomingString,
|
||||
})
|
||||
end,
|
||||
showTableDiff = function(oldTable: { [any]: any? }, newTable: { [any]: any? })
|
||||
@@ -566,8 +566,8 @@ function ConnectedPage:render()
|
||||
anchorPoint = Vector2.new(0, 0),
|
||||
transparency = self.props.transparency,
|
||||
|
||||
oldString = self.state.oldString,
|
||||
newString = self.state.newString,
|
||||
currentString = self.state.currentString,
|
||||
incomingString = self.state.incomingString,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -4,6 +4,8 @@ local Packages = Rojo.Packages
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
local Theme = require(Plugin.App.Theme)
|
||||
|
||||
local Spinner = require(Plugin.App.Components.Spinner)
|
||||
|
||||
local e = Roact.createElement
|
||||
@@ -11,11 +13,35 @@ local e = Roact.createElement
|
||||
local ConnectingPage = Roact.Component:extend("ConnectingPage")
|
||||
|
||||
function ConnectingPage:render()
|
||||
return e(Spinner, {
|
||||
return Theme.with(function(theme)
|
||||
return e("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
}, {
|
||||
Spinner = e(Spinner, {
|
||||
position = UDim2.new(0.5, 0, 0.5, 0),
|
||||
anchorPoint = Vector2.new(0.5, 0.5),
|
||||
transparency = self.props.transparency,
|
||||
}),
|
||||
Text = if type(self.props.text) == "string" and #self.props.text > 0
|
||||
then e("TextLabel", {
|
||||
Text = self.props.text,
|
||||
Position = UDim2.new(0.5, 0, 0.5, 30),
|
||||
Size = UDim2.new(1, -40, 0.5, -40),
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
TextXAlignment = Enum.TextXAlignment.Center,
|
||||
TextYAlignment = Enum.TextYAlignment.Top,
|
||||
RichText = true,
|
||||
FontFace = theme.Font.Thin,
|
||||
TextSize = theme.TextSize.Medium,
|
||||
TextColor3 = theme.SubTextColor,
|
||||
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||
TextTransparency = self.props.transparency,
|
||||
BackgroundTransparency = 1,
|
||||
})
|
||||
else nil,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
return ConnectingPage
|
||||
|
||||
@@ -44,7 +44,7 @@ end
|
||||
local function blendAlpha(alphaValues)
|
||||
local alpha = 0
|
||||
|
||||
for _, value in pairs(alphaValues) do
|
||||
for _, value in alphaValues do
|
||||
alpha = alpha + (1 - alpha) * value
|
||||
end
|
||||
|
||||
|
||||
@@ -174,6 +174,8 @@ function App:init()
|
||||
end
|
||||
|
||||
function App:willUnmount()
|
||||
self:endSession()
|
||||
|
||||
self.waypointConnection:Disconnect()
|
||||
self.confirmationBindable:Destroy()
|
||||
|
||||
@@ -595,6 +597,12 @@ function App:startSession()
|
||||
twoWaySync = Settings:get("twoWaySync"),
|
||||
})
|
||||
|
||||
serveSession:setUpdateLoadingTextCallback(function(text: string)
|
||||
self:setState({
|
||||
connectingText = text,
|
||||
})
|
||||
end)
|
||||
|
||||
self.cleanupPrecommit = serveSession:hookPrecommit(function(patch, instanceMap)
|
||||
-- Build new tree for patch
|
||||
self:setState({
|
||||
@@ -602,44 +610,30 @@ function App:startSession()
|
||||
})
|
||||
end)
|
||||
self.cleanupPostcommit = serveSession: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 = DateTime.now().UnixTimestamp
|
||||
local old = self.state.patchData
|
||||
self:setState(function(prevState)
|
||||
local oldPatchData = prevState.patchData
|
||||
local newPatchData = {
|
||||
patch = patch,
|
||||
unapplied = unappliedPatch,
|
||||
timestamp = now,
|
||||
}
|
||||
|
||||
if PatchSet.isEmpty(patch) then
|
||||
-- Ignore empty patch, but update timestamp
|
||||
self:setState({
|
||||
patchData = {
|
||||
patch = old.patch,
|
||||
unapplied = old.unapplied,
|
||||
timestamp = now,
|
||||
},
|
||||
})
|
||||
return
|
||||
-- Keep existing patch info, but use new timestamp
|
||||
newPatchData.patch = oldPatchData.patch
|
||||
newPatchData.unapplied = oldPatchData.unapplied
|
||||
elseif now - oldPatchData.timestamp < 2 then
|
||||
-- Patches that apply in the same second are combined for human clarity
|
||||
newPatchData.patch = PatchSet.assign(PatchSet.newEmpty(), oldPatchData.patch, patch)
|
||||
newPatchData.unapplied = PatchSet.assign(PatchSet.newEmpty(), oldPatchData.unapplied, unappliedPatch)
|
||||
end
|
||||
|
||||
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
|
||||
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,
|
||||
},
|
||||
})
|
||||
return {
|
||||
patchTree = PatchTree.updateMetadata(prevState.patchTree, patch, instanceMap, unappliedPatch),
|
||||
patchData = newPatchData,
|
||||
}
|
||||
end)
|
||||
end)
|
||||
|
||||
serveSession:onStatusChanged(function(status, details)
|
||||
@@ -773,11 +767,13 @@ function App:startSession()
|
||||
end
|
||||
end
|
||||
|
||||
self:setState({
|
||||
connectingText = "Computing diff view...",
|
||||
})
|
||||
self:setState({
|
||||
appStatus = AppStatus.Confirming,
|
||||
patchTree = PatchTree.build(patch, instanceMap, { "Property", "Current", "Incoming" }),
|
||||
confirmData = {
|
||||
instanceMap = instanceMap,
|
||||
patch = patch,
|
||||
serverInfo = serverInfo,
|
||||
},
|
||||
toolbarIcon = Assets.Images.PluginButton,
|
||||
@@ -888,6 +884,7 @@ function App:render()
|
||||
|
||||
ConfirmingPage = createPageElement(AppStatus.Confirming, {
|
||||
confirmData = self.state.confirmData,
|
||||
patchTree = self.state.patchTree,
|
||||
createPopup = not self.state.guiEnabled,
|
||||
|
||||
onAbort = function()
|
||||
@@ -901,7 +898,9 @@ function App:render()
|
||||
end,
|
||||
}),
|
||||
|
||||
Connecting = createPageElement(AppStatus.Connecting),
|
||||
Connecting = createPageElement(AppStatus.Connecting, {
|
||||
text = self.state.connectingText,
|
||||
}),
|
||||
|
||||
Connected = createPageElement(AppStatus.Connected, {
|
||||
projectName = self.state.projectName,
|
||||
|
||||
@@ -74,7 +74,7 @@ local Assets = {
|
||||
local function guardForTypos(name, map)
|
||||
strict(name, map)
|
||||
|
||||
for key, child in pairs(map) do
|
||||
for key, child in map do
|
||||
if type(child) == "table" then
|
||||
guardForTypos(("%s.%s"):format(name, key), child)
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ local encodePatchUpdate = require(script.Parent.encodePatchUpdate)
|
||||
return function(instanceMap, propertyChanges)
|
||||
local patch = PatchSet.newEmpty()
|
||||
|
||||
for instance, properties in pairs(propertyChanges) do
|
||||
for instance, properties in propertyChanges do
|
||||
local instanceId = instanceMap.fromInstances[instance]
|
||||
|
||||
if instanceId == nil then
|
||||
|
||||
@@ -10,7 +10,7 @@ return function(instance, instanceId, properties)
|
||||
changedProperties = {},
|
||||
}
|
||||
|
||||
for propertyName in pairs(properties) do
|
||||
for propertyName in properties do
|
||||
if propertyName == "Name" then
|
||||
update.changedName = instance.Name
|
||||
else
|
||||
|
||||
@@ -21,7 +21,7 @@ return strict("Config", {
|
||||
codename = "Epiphany",
|
||||
version = realVersion,
|
||||
expectedServerVersionString = ("%d.%d or newer"):format(realVersion[1], realVersion[2]),
|
||||
protocolVersion = 4,
|
||||
protocolVersion = 5,
|
||||
defaultHost = "localhost",
|
||||
defaultPort = "34872",
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ local function merge(...)
|
||||
local source = select(i, ...)
|
||||
|
||||
if source ~= nil then
|
||||
for key, value in pairs(source) do
|
||||
for key, value in source do
|
||||
if value == None then
|
||||
output[key] = nil
|
||||
else
|
||||
|
||||
@@ -63,7 +63,7 @@ function InstanceMap:__fmtDebug(output)
|
||||
-- Collect all of the entries in the InstanceMap and sort them by their
|
||||
-- label, which helps make our output deterministic.
|
||||
local entries = {}
|
||||
for id, instance in pairs(self.fromIds) do
|
||||
for id, instance in self.fromIds do
|
||||
local label = string.format("%s (%s)", instance:GetFullName(), instance.ClassName)
|
||||
|
||||
table.insert(entries, { id, label })
|
||||
@@ -73,7 +73,7 @@ function InstanceMap:__fmtDebug(output)
|
||||
return a[2] < b[2]
|
||||
end)
|
||||
|
||||
for _, entry in ipairs(entries) do
|
||||
for _, entry in entries do
|
||||
output:writeLine("{}: {}", entry[1], entry[2])
|
||||
end
|
||||
|
||||
@@ -227,7 +227,7 @@ function InstanceMap:__disconnectSignals(instance)
|
||||
-- around the extra table. ValueBase objects force us to use multiple
|
||||
-- signals to emulate the Instance.Changed event, however.
|
||||
if typeof(signals) == "table" then
|
||||
for _, signal in ipairs(signals) do
|
||||
for _, signal in signals do
|
||||
signal:Disconnect()
|
||||
end
|
||||
else
|
||||
|
||||
@@ -16,6 +16,14 @@ local Types = require(Plugin.Types)
|
||||
local decodeValue = require(Plugin.Reconciler.decodeValue)
|
||||
local getProperty = require(Plugin.Reconciler.getProperty)
|
||||
|
||||
local function yieldIfNeeded(clock)
|
||||
if os.clock() - clock > 1 / 20 then
|
||||
task.wait()
|
||||
return os.clock()
|
||||
end
|
||||
return clock
|
||||
end
|
||||
|
||||
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
|
||||
@@ -132,7 +140,6 @@ end
|
||||
-- props must contain id, and cannot contain children or parentId
|
||||
-- other than those three, it can hold anything
|
||||
function Tree:addNode(parent, props)
|
||||
Timer.start("Tree:addNode")
|
||||
assert(props.id, "props must contain id")
|
||||
|
||||
parent = parent or "ROOT"
|
||||
@@ -143,7 +150,6 @@ function Tree:addNode(parent, props)
|
||||
for k, v in props do
|
||||
node[k] = v
|
||||
end
|
||||
Timer.stop()
|
||||
return node
|
||||
end
|
||||
|
||||
@@ -154,25 +160,25 @@ function Tree:addNode(parent, props)
|
||||
local parentNode = self:getNode(parent)
|
||||
if not parentNode then
|
||||
Log.warn("Failed to create node since parent doesnt exist: {}, {}", parent, props)
|
||||
Timer.stop()
|
||||
return
|
||||
end
|
||||
|
||||
parentNode.children[node.id] = node
|
||||
self.idToNode[node.id] = node
|
||||
|
||||
Timer.stop()
|
||||
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(previousId: string?, ancestryIds: { string }, patch, instanceMap)
|
||||
Timer.start("Tree:buildAncestryNodes")
|
||||
local clock = os.clock()
|
||||
-- Build nodes for ancestry by going up the tree
|
||||
previousId = previousId or "ROOT"
|
||||
|
||||
for _, ancestorId in ancestryIds do
|
||||
clock = yieldIfNeeded(clock)
|
||||
|
||||
local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
|
||||
if not value then
|
||||
Log.warn("Failed to find ancestor object for " .. ancestorId)
|
||||
@@ -186,8 +192,6 @@ function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, p
|
||||
})
|
||||
previousId = ancestorId
|
||||
end
|
||||
|
||||
Timer.stop()
|
||||
end
|
||||
|
||||
local PatchTree = {}
|
||||
@@ -196,12 +200,16 @@ local PatchTree = {}
|
||||
-- uses changeListHeaders in node.changeList
|
||||
function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
Timer.start("PatchTree.build")
|
||||
local clock = os.clock()
|
||||
|
||||
local tree = Tree.new()
|
||||
|
||||
local knownAncestors = {}
|
||||
|
||||
Timer.start("patch.updated")
|
||||
for _, change in patch.updated do
|
||||
clock = yieldIfNeeded(clock)
|
||||
|
||||
local instance = instanceMap.fromIds[change.id]
|
||||
if not instance then
|
||||
continue
|
||||
@@ -281,6 +289,8 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
|
||||
Timer.start("patch.removed")
|
||||
for _, idOrInstance in patch.removed do
|
||||
clock = yieldIfNeeded(clock)
|
||||
|
||||
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
|
||||
@@ -325,6 +335,8 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||
|
||||
Timer.start("patch.added")
|
||||
for id, change in patch.added do
|
||||
clock = yieldIfNeeded(clock)
|
||||
|
||||
-- Gather ancestors from existing DOM or future additions
|
||||
local ancestryIds = {}
|
||||
local parentId = change.Parent
|
||||
|
||||
@@ -38,13 +38,13 @@ local function trueEquals(a, b): boolean
|
||||
-- For tables, try recursive deep equality
|
||||
if typeA == "table" and typeB == "table" then
|
||||
local checkedKeys = {}
|
||||
for key, value in pairs(a) do
|
||||
for key, value in a do
|
||||
checkedKeys[key] = true
|
||||
if not trueEquals(value, b[key]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for key, value in pairs(b) do
|
||||
for key, value in b do
|
||||
if checkedKeys[key] then
|
||||
continue
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ return function()
|
||||
local function size(dict)
|
||||
local len = 0
|
||||
|
||||
for _ in pairs(dict) do
|
||||
for _ in dict do
|
||||
len = len + 1
|
||||
end
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ local function hydrate(instanceMap, virtualInstances, rootId, rootInstance)
|
||||
for _, childId in ipairs(virtualInstance.Children) do
|
||||
local virtualChild = virtualInstances[childId]
|
||||
|
||||
for childIndex, childInstance in ipairs(existingChildren) do
|
||||
for childIndex, childInstance in existingChildren do
|
||||
if not isExistingChildVisited[childIndex] then
|
||||
-- We guard accessing Name and ClassName in order to avoid
|
||||
-- tripping over children of DataModel that Rojo won't have
|
||||
|
||||
@@ -126,7 +126,7 @@ function applyDeferredRefs(instanceMap, deferredRefs, unappliedPatch)
|
||||
})
|
||||
end
|
||||
|
||||
for _, entry in ipairs(deferredRefs) do
|
||||
for _, entry in deferredRefs do
|
||||
local _, refId = next(entry.virtualValue)
|
||||
|
||||
if refId == nil then
|
||||
|
||||
@@ -12,7 +12,7 @@ return function()
|
||||
local function size(dict)
|
||||
local len = 0
|
||||
|
||||
for _ in pairs(dict) do
|
||||
for _ in dict do
|
||||
len = len + 1
|
||||
end
|
||||
|
||||
|
||||
@@ -48,6 +48,12 @@ local function debugPatch(object)
|
||||
end)
|
||||
end
|
||||
|
||||
local function attemptReparent(instance, parent)
|
||||
return pcall(function()
|
||||
instance.Parent = parent
|
||||
end)
|
||||
end
|
||||
|
||||
local ServeSession = {}
|
||||
ServeSession.__index = ServeSession
|
||||
|
||||
@@ -101,6 +107,7 @@ function ServeSession.new(options)
|
||||
__connections = connections,
|
||||
__precommitCallbacks = {},
|
||||
__postcommitCallbacks = {},
|
||||
__updateLoadingText = function() end,
|
||||
}
|
||||
|
||||
setmetatable(self, ServeSession)
|
||||
@@ -131,6 +138,14 @@ function ServeSession:setConfirmCallback(callback)
|
||||
self.__userConfirmCallback = callback
|
||||
end
|
||||
|
||||
function ServeSession:setUpdateLoadingTextCallback(callback)
|
||||
self.__updateLoadingText = callback
|
||||
end
|
||||
|
||||
function ServeSession:setLoadingText(text: string)
|
||||
self.__updateLoadingText(text)
|
||||
end
|
||||
|
||||
--[=[
|
||||
Hooks a function to run before patch application.
|
||||
The provided function is called with the incoming patch and an InstanceMap
|
||||
@@ -175,15 +190,31 @@ end
|
||||
|
||||
function ServeSession:start()
|
||||
self:__setStatus(Status.Connecting)
|
||||
self:setLoadingText("Connecting to server...")
|
||||
|
||||
self.__apiContext
|
||||
:connect()
|
||||
:andThen(function(serverInfo)
|
||||
self:setLoadingText("Loading initial data from server...")
|
||||
return self:__initialSync(serverInfo):andThen(function()
|
||||
self:setLoadingText("Starting sync loop...")
|
||||
self:__setStatus(Status.Connected, serverInfo.projectName)
|
||||
self:__applyGameAndPlaceId(serverInfo)
|
||||
|
||||
return self:__mainSyncLoop()
|
||||
return self.__apiContext:connectWebSocket({
|
||||
["messages"] = function(messagesPacket)
|
||||
if self.__status == Status.Disconnected then
|
||||
return
|
||||
end
|
||||
|
||||
Log.debug("Received {} messages from Rojo server", #messagesPacket.messages)
|
||||
|
||||
for _, message in messagesPacket.messages do
|
||||
self:__applyPatch(message)
|
||||
end
|
||||
self.__apiContext:setMessageCursor(messagesPacket.messageCursor)
|
||||
end,
|
||||
})
|
||||
end)
|
||||
end)
|
||||
:catch(function(err)
|
||||
@@ -291,18 +322,52 @@ function ServeSession:__replaceInstances(idList)
|
||||
|
||||
for id, replacement in replacements do
|
||||
local oldInstance = self.__instanceMap.fromIds[id]
|
||||
if not oldInstance then
|
||||
-- TODO: Why would this happen?
|
||||
Log.warn("Instance {} not found in InstanceMap during sync replacement", id)
|
||||
continue
|
||||
end
|
||||
|
||||
self.__instanceMap:insert(id, replacement)
|
||||
Log.trace("Swapping Instance {} out via api/models/ endpoint", id)
|
||||
local oldParent = oldInstance.Parent
|
||||
for _, child in oldInstance:GetChildren() do
|
||||
child.Parent = replacement
|
||||
-- Some children cannot be reparented, such as a TouchTransmitter
|
||||
local reparentSuccess, reparentError = attemptReparent(child, replacement)
|
||||
if not reparentSuccess then
|
||||
Log.warn(
|
||||
"Could not reparent child {} of instance {} during sync replacement: {}",
|
||||
child.Name,
|
||||
oldInstance.Name,
|
||||
reparentError
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
replacement.Parent = oldParent
|
||||
-- ChangeHistoryService doesn't like it if an Instance has been
|
||||
-- Destroyed. So, we have to accept the potential memory hit and
|
||||
-- just set the parent to `nil`.
|
||||
oldInstance.Parent = nil
|
||||
local deleteSuccess, deleteError = attemptReparent(oldInstance, nil)
|
||||
local replaceSuccess, replaceError = attemptReparent(replacement, oldParent)
|
||||
|
||||
if not (deleteSuccess and replaceSuccess) then
|
||||
Log.warn(
|
||||
"Could not swap instances {} and {} during sync replacement: {}",
|
||||
oldInstance.Name,
|
||||
replacement.Name,
|
||||
(deleteError or "") .. "\n" .. (replaceError or "")
|
||||
)
|
||||
|
||||
-- We need to revert the failed swap to avoid losing the old instance and children.
|
||||
for _, child in replacement:GetChildren() do
|
||||
attemptReparent(child, oldInstance)
|
||||
end
|
||||
attemptReparent(oldInstance, oldParent)
|
||||
|
||||
-- Our replacement should never have existed in the first place, so we can just destroy it.
|
||||
replacement:Destroy()
|
||||
continue
|
||||
end
|
||||
|
||||
if selectionMap[oldInstance] then
|
||||
-- This is a bit funky, but it saves the order of Selection
|
||||
@@ -349,18 +414,11 @@ function ServeSession:__applyPatch(patch)
|
||||
error(unappliedPatch)
|
||||
end
|
||||
|
||||
if PatchSet.isEmpty(unappliedPatch) then
|
||||
if historyRecording then
|
||||
ChangeHistoryService:FinishRecording(historyRecording, Enum.FinishRecordingOperation.Commit)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if Settings:get("enableSyncFallback") and not PatchSet.isEmpty(unappliedPatch) then
|
||||
-- Some changes did not apply, let's try replacing them instead
|
||||
local addedIdList = PatchSet.addedIdList(unappliedPatch)
|
||||
local updatedIdList = PatchSet.updatedIdList(unappliedPatch)
|
||||
|
||||
local actualUnappliedPatches = PatchSet.newEmpty()
|
||||
if Settings:get("enableSyncFallback") then
|
||||
Log.debug("ServeSession:__replaceInstances(unappliedPatch.added)")
|
||||
Timer.start("ServeSession:__replaceInstances(unappliedPatch.added)")
|
||||
local addSuccess, unappliedAddedRefs = self:__replaceInstances(addedIdList)
|
||||
@@ -371,20 +429,18 @@ function ServeSession:__applyPatch(patch)
|
||||
local updateSuccess, unappliedUpdateRefs = self:__replaceInstances(updatedIdList)
|
||||
Timer.stop()
|
||||
|
||||
-- Update the unapplied patch to reflect which Instances were replaced successfully
|
||||
if addSuccess then
|
||||
table.clear(unappliedPatch.added)
|
||||
PatchSet.assign(actualUnappliedPatches, unappliedAddedRefs)
|
||||
PatchSet.assign(unappliedPatch, unappliedAddedRefs)
|
||||
end
|
||||
if updateSuccess then
|
||||
table.clear(unappliedPatch.updated)
|
||||
PatchSet.assign(actualUnappliedPatches, unappliedUpdateRefs)
|
||||
PatchSet.assign(unappliedPatch, unappliedUpdateRefs)
|
||||
end
|
||||
else
|
||||
Log.debug("Skipping ServeSession:__replaceInstances because of setting")
|
||||
end
|
||||
PatchSet.assign(actualUnappliedPatches, unappliedPatch)
|
||||
|
||||
if not PatchSet.isEmpty(actualUnappliedPatches) then
|
||||
if not PatchSet.isEmpty(unappliedPatch) then
|
||||
Log.debug(
|
||||
"Could not apply all changes requested by the Rojo server:\n{}",
|
||||
PatchSet.humanSummary(self.__instanceMap, unappliedPatch)
|
||||
@@ -396,7 +452,7 @@ function ServeSession:__applyPatch(patch)
|
||||
-- guaranteed to be called after the commit
|
||||
for _, callback in self.__postcommitCallbacks do
|
||||
task.spawn(function()
|
||||
local success, err = pcall(callback, patch, self.__instanceMap, actualUnappliedPatches)
|
||||
local success, err = pcall(callback, patch, self.__instanceMap, unappliedPatch)
|
||||
if not success then
|
||||
Log.warn("Postcommit hook errored: {}", err)
|
||||
end
|
||||
@@ -418,11 +474,13 @@ function ServeSession:__initialSync(serverInfo)
|
||||
-- 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:setLoadingText("Hydrating instance map...")
|
||||
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...")
|
||||
self:setLoadingText("Finding differences between server and Studio...")
|
||||
local success, catchUpPatch =
|
||||
self.__reconciler:diff(readResponseBody.instances, serverInfo.rootInstanceId, game)
|
||||
|
||||
@@ -491,40 +549,6 @@ function ServeSession:__initialSync(serverInfo)
|
||||
end)
|
||||
end
|
||||
|
||||
function ServeSession:__mainSyncLoop()
|
||||
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
|
||||
|
||||
Log.trace("Serve session {} retrieved {} messages", tostring(self), #messages)
|
||||
|
||||
for _, message in messages do
|
||||
self:__applyPatch(message)
|
||||
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
|
||||
|
||||
-- We are no longer connected, so we resolve the promise
|
||||
resolve()
|
||||
end)
|
||||
end
|
||||
|
||||
function ServeSession:__stopInternal(err)
|
||||
self:__setStatus(Status.Disconnected, err)
|
||||
self.__apiContext:disconnect()
|
||||
|
||||
@@ -49,12 +49,21 @@ local ApiReadResponse = t.interface({
|
||||
instances = t.map(RbxId, ApiInstance),
|
||||
})
|
||||
|
||||
local ApiSubscribeResponse = t.interface({
|
||||
sessionId = t.string,
|
||||
local SocketPacketType = t.union(t.literal("messages"))
|
||||
|
||||
local MessagesPacket = t.interface({
|
||||
messageCursor = t.number,
|
||||
messages = t.array(ApiSubscribeMessage),
|
||||
})
|
||||
|
||||
local SocketPacketBody = t.union(MessagesPacket)
|
||||
|
||||
local ApiSocketPacket = t.interface({
|
||||
sessionId = t.string,
|
||||
packetType = SocketPacketType,
|
||||
body = SocketPacketBody,
|
||||
})
|
||||
|
||||
local ApiSerializeResponse = t.interface({
|
||||
sessionId = t.string,
|
||||
modelContents = t.buffer,
|
||||
@@ -85,7 +94,7 @@ return strict("Types", {
|
||||
|
||||
ApiInfoResponse = ApiInfoResponse,
|
||||
ApiReadResponse = ApiReadResponse,
|
||||
ApiSubscribeResponse = ApiSubscribeResponse,
|
||||
ApiSocketPacket = ApiSocketPacket,
|
||||
ApiError = ApiError,
|
||||
|
||||
ApiInstance = ApiInstance,
|
||||
|
||||
@@ -9,7 +9,7 @@ local gatherAssetUrlsRecursive
|
||||
function gatherAssetUrlsRecursive(currentTable, currentUrls)
|
||||
currentUrls = currentUrls or {}
|
||||
|
||||
for _, value in pairs(currentTable) do
|
||||
for _, value in currentTable do
|
||||
if typeof(value) == "string" then
|
||||
table.insert(currentUrls, value)
|
||||
elseif typeof(value) == "table" then
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: add_folder
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-3:
|
||||
Children: []
|
||||
@@ -16,4 +17,5 @@ messages:
|
||||
Properties: {}
|
||||
removed: []
|
||||
updated: []
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
@@ -7,7 +7,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: optional
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: edit_init
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
@@ -15,5 +15,5 @@ messages:
|
||||
Source:
|
||||
String: "-- Edited contents"
|
||||
id: id-2
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: empty
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: empty_folder
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-3:
|
||||
Children: []
|
||||
@@ -18,4 +19,5 @@ messages:
|
||||
Bool: false
|
||||
removed: []
|
||||
updated: []
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: forced_parent
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: meshpart
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-6:
|
||||
Children: []
|
||||
@@ -24,4 +25,5 @@ messages:
|
||||
changedName: ~
|
||||
changedProperties: {}
|
||||
id: id-3
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: move_folder_of_stuff
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-10:
|
||||
Children: []
|
||||
@@ -137,5 +137,5 @@ messages:
|
||||
String: "File #5"
|
||||
removed: []
|
||||
updated: []
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: top-level
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: no_name_project
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: no_name_top_level_project
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: pivot_migration
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: ref_properties
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: ref_properties
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
@@ -14,4 +15,5 @@ messages:
|
||||
Scale:
|
||||
Float32: 1
|
||||
id: id-8
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: ref_properties_remove
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed:
|
||||
- id-4
|
||||
updated: []
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-11:
|
||||
Children: []
|
||||
@@ -44,4 +45,5 @@ messages:
|
||||
String: model target 2
|
||||
PrimaryPart: ~
|
||||
id: id-8
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: remove_file
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed:
|
||||
- id-3
|
||||
updated: []
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: scripts
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
@@ -15,5 +15,5 @@ messages:
|
||||
Source:
|
||||
String: Updated foo!
|
||||
id: id-4
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: sync_rule_alone
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: sync_rule_complex
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: sync_rule_no_extension
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: sync_rule_no_name_project
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing OnlyOneCopy/child_of_one.luau
|
||||
Writing ReplicatedStorage/child_replicated_storage.luau
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/csv.csv
|
||||
Writing src/csv_init/init.csv
|
||||
Writing src/csv_init
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing container.model.json
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/int_value.model.json
|
||||
Writing src/subfolder/string_value.txt
|
||||
Writing src/subfolder
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/non-init.luau
|
||||
Writing src/init-file
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: src/Message.rbxm
|
||||
---
|
||||
num_types: 1
|
||||
num_instances: 1
|
||||
chunks:
|
||||
- Inst:
|
||||
type_id: 0
|
||||
type_name: Message
|
||||
object_format: 0
|
||||
referents:
|
||||
- 0
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: AttributesSerialize
|
||||
prop_type: String
|
||||
values:
|
||||
- ""
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: Capabilities
|
||||
prop_type: SecurityCapabilities
|
||||
values:
|
||||
- 0
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: HistoryId
|
||||
prop_type: UniqueId
|
||||
values:
|
||||
- "00000000000000000000000000000000"
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: Name
|
||||
prop_type: String
|
||||
values:
|
||||
- Message
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: DefinesCapabilities
|
||||
prop_type: Bool
|
||||
values:
|
||||
- false
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: SourceAssetId
|
||||
prop_type: Int64
|
||||
values:
|
||||
- -1
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: Tags
|
||||
prop_type: String
|
||||
values:
|
||||
- ""
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: Text
|
||||
prop_type: String
|
||||
values:
|
||||
- This message should be written to the disk.
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: UniqueId
|
||||
prop_type: UniqueId
|
||||
values:
|
||||
- 2030b7775c30713f085b9d4800005a8b
|
||||
- Prnt:
|
||||
version: 0
|
||||
links:
|
||||
- - 0
|
||||
- -1
|
||||
- End
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/Message.rbxm
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/IncludeMe/.gitkeep
|
||||
Writing src/IncludeMe
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/dir_with_meta/init.meta.json
|
||||
Writing src/model_json.model.json
|
||||
Writing src/project_json.project.json
|
||||
Writing src/dir_with_meta
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing nested.project.json
|
||||
Writing string_value.txt
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/modules/ClientModule.luau
|
||||
Writing src/modules/ServerModule.luau
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/init.luau
|
||||
Writing src
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing attribute_mismatch.luau
|
||||
Writing property_mismatch.project.json
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: src/ChildWithDuplicates.rbxm
|
||||
---
|
||||
num_types: 1
|
||||
num_instances: 3
|
||||
chunks:
|
||||
- Inst:
|
||||
type_id: 0
|
||||
type_name: Folder
|
||||
object_format: 0
|
||||
referents:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: AttributesSerialize
|
||||
prop_type: String
|
||||
values:
|
||||
- ""
|
||||
- ""
|
||||
- ""
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: Capabilities
|
||||
prop_type: SecurityCapabilities
|
||||
values:
|
||||
- 0
|
||||
- 0
|
||||
- 0
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: Name
|
||||
prop_type: String
|
||||
values:
|
||||
- DuplicateChild
|
||||
- DuplicateChild
|
||||
- ChildWithDuplicates
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: DefinesCapabilities
|
||||
prop_type: Bool
|
||||
values:
|
||||
- false
|
||||
- false
|
||||
- false
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: SourceAssetId
|
||||
prop_type: Int64
|
||||
values:
|
||||
- -1
|
||||
- -1
|
||||
- -1
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: Tags
|
||||
prop_type: String
|
||||
values:
|
||||
- ""
|
||||
- ""
|
||||
- ""
|
||||
- Prnt:
|
||||
version: 0
|
||||
links:
|
||||
- - 0
|
||||
- 2
|
||||
- - 1
|
||||
- 2
|
||||
- - 2
|
||||
- -1
|
||||
- End
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/ChildWithDuplicates.rbxm
|
||||
Writing src/ChildWithoutDuplicates/Child/.gitkeep
|
||||
Writing src/ChildWithoutDuplicates
|
||||
Writing src/ChildWithoutDuplicates/Child
|
||||
Removing src/ChildWithDuplicates
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/pointer.model.json
|
||||
Writing src/target.model.json
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/pointer.model.json
|
||||
Writing src/target.meta.json
|
||||
Writing src/target.txt
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/Pointer_2.model.json
|
||||
Writing src/Target_2.model.json
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
Writing src/Pointer1.model.json
|
||||
Writing src/Pointer2.model.json
|
||||
Writing src/Pointer3.model.json
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
source: tests/rojo_test/syncback_util.rs
|
||||
expression: src/rbxm.rbxm
|
||||
---
|
||||
num_types: 1
|
||||
num_instances: 1
|
||||
chunks:
|
||||
- Inst:
|
||||
type_id: 0
|
||||
type_name: Folder
|
||||
object_format: 0
|
||||
referents:
|
||||
- 0
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: AttributesSerialize
|
||||
prop_type: String
|
||||
values:
|
||||
- ""
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: Capabilities
|
||||
prop_type: SecurityCapabilities
|
||||
values:
|
||||
- 0
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: Name
|
||||
prop_type: String
|
||||
values:
|
||||
- rbxm
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: DefinesCapabilities
|
||||
prop_type: Bool
|
||||
values:
|
||||
- false
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: SourceAssetId
|
||||
prop_type: Int64
|
||||
values:
|
||||
- -1
|
||||
- Prop:
|
||||
type_id: 0
|
||||
prop_name: Tags
|
||||
prop_type: String
|
||||
values:
|
||||
- rbxmx
|
||||
- Prnt:
|
||||
version: 0
|
||||
links:
|
||||
- - 0
|
||||
- -1
|
||||
- End
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user