forked from rojo-rbx/rojo
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6e9939d6c | |||
|
5957368c04
|
|||
|
78916c8a63
|
|||
|
791ccfcfd1
|
|||
|
3500ebe02a
|
|||
|
|
2a1102fc55 | ||
|
|
02b41133f8 | ||
|
0e1364945f
|
|||
| 3a6aae65f7 | |||
| d13d229eef | |||
| 9a485d88ce | |||
|
020d72faef
|
|||
|
60d150f4c6
|
|||
|
73dab330b5
|
|||
|
790312a5b0
|
|||
|
5c396322d9
|
|||
|
37e44e474a
|
|||
|
|
d08780fc14 | ||
|
|
b89cc7f398 | ||
|
|
42568b9709 | ||
|
|
87f58e0a55 | ||
|
|
a61a1bef55 | ||
|
|
a99e877b7c | ||
|
|
93e9c51204 | ||
|
|
015b5bda14 | ||
|
|
2b47861a4f | ||
|
|
9b5a07191b | ||
|
|
071b6e7e23 | ||
|
|
31ec216a95 | ||
|
|
ea70d89291 | ||
|
|
03410ced6d |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,3 +23,6 @@
|
||||
# Macos file system junk
|
||||
._*
|
||||
.DS_STORE
|
||||
|
||||
# JetBrains IDEs
|
||||
/.idea/
|
||||
|
||||
56
CHANGELOG.md
56
CHANGELOG.md
@@ -30,6 +30,62 @@ Making a new release? Simply add the new header with the version and date undern
|
||||
-->
|
||||
|
||||
## Unreleased
|
||||
* Fixed a bug caused by having reference properties (such as `ObjectValue.Value`) that point to an Instance not included in syncback. ([#1179])
|
||||
* Implemented support for the "name" property in meta/model JSON files. ([#1187])
|
||||
* Fixed instance replacement fallback failing when too many instances needed to be replaced. ([#1192])
|
||||
* Fixed a bug where MacOS paths weren't being handled correctly. ([#1201])
|
||||
|
||||
[#1179]: https://github.com/rojo-rbx/rojo/pull/1179
|
||||
[#1187]: https://github.com/rojo-rbx/rojo/pull/1187
|
||||
[#1192]: https://github.com/rojo-rbx/rojo/pull/1192
|
||||
[#1201]: https://github.com/rojo-rbx/rojo/pull/1201
|
||||
|
||||
## [7.7.0-rc.1] (November 27th, 2025)
|
||||
|
||||
* Fixed a bug where passing `--skip-git` to `rojo init` would still create a file named `gitignore.txt` ([#1172])
|
||||
* A new command `rojo syncback` has been added. It can be used as `rojo syncback [path to project] --input [path to file]`. ([#937])
|
||||
This command takes a Roblox file and pulls Instances out of it and places them in the correct position in the provided project.
|
||||
Syncback is primarily controlled by the project file. Any Instances who are either referenced in the project file or a descendant
|
||||
of one that is will be placed in an appropriate location.
|
||||
|
||||
In addition, a new field has been added to project files, `syncbackRules` to control how it behaves:
|
||||
|
||||
```json
|
||||
{
|
||||
"syncbackRules": {
|
||||
"ignoreTrees": [
|
||||
"ServerStorage/ImportantSecrets",
|
||||
],
|
||||
"ignorePaths": [
|
||||
"src/ServerStorage/Secrets/*"
|
||||
],
|
||||
"ignoreProperties": {
|
||||
"BasePart": ["Color"]
|
||||
},
|
||||
"syncCurrentCamera": false,
|
||||
"syncUnscriptable": true,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A brief explanation of each field:
|
||||
|
||||
- `ignoreTrees` is a list of paths in the **roblox file** that should be ignored
|
||||
- `ignorePaths` is a list of paths in the **file system** that should be ignored
|
||||
- `ignoreProperties` is a list of properties that won't be synced back
|
||||
- `syncCurrentCamera` is a toggle for whether to sync back the Workspace's CurrentCamera. Defaults to `false`.
|
||||
- `syncUnscriptable` is a toggle for whether to sync back properties that cannot be set by the Roblox Studio plugin. Defaults to `true`.
|
||||
|
||||
* Fixed bugs and improved performance & UX for the script diff viewer ([#994])
|
||||
* Rebuilt the internal communication between the server and plugin to use [websockets](https://devforum.roblox.com/t/websockets-support-in-studio-is-now-available/4021932/1) instead of [long polling](https://en.wikipedia.org/wiki/Push_technology#Long_polling) ([#1142])
|
||||
* Added support for `.jsonc` files for all JSON-related files (e.g. `.project.jsonc` and `.meta.jsonc`) to accompany JSONC support ([#1159])
|
||||
|
||||
[7.7.0-rc.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.7.0-rc.1
|
||||
[#937]: https://github.com/rojo-rbx/rojo/pull/937
|
||||
[#994]: https://github.com/rojo-rbx/rojo/pull/994
|
||||
[#1142]: https://github.com/rojo-rbx/rojo/pull/1142
|
||||
[#1159]: https://github.com/rojo-rbx/rojo/pull/1159
|
||||
[#1172]: https://github.com/rojo-rbx/rojo/pull/1172
|
||||
|
||||
## [7.6.1] (November 6th, 2025)
|
||||
|
||||
|
||||
1617
Cargo.lock
generated
1617
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rojo"
|
||||
version = "7.6.1"
|
||||
version = "7.7.0-rc.1"
|
||||
rust-version = "1.88"
|
||||
authors = [
|
||||
"Lucien Greathouse <me@lpghatguy.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.1"
|
||||
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"
|
||||
@@ -87,16 +90,21 @@ roblox_install = "1.0.0"
|
||||
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
||||
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"
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
# memofs Changelog
|
||||
|
||||
## Unreleased Changes
|
||||
* Added `Vfs::canonicalize`. [#1201]
|
||||
|
||||
## 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]
|
||||
|
||||
@@ -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>",
|
||||
@@ -19,3 +19,6 @@ crossbeam-channel = "0.5.12"
|
||||
fs-err = "2.11.0"
|
||||
notify = "4.0.17"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10.1"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -212,6 +232,33 @@ impl VfsBackend for InMemoryFs {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We rely on Rojo to prepend cwd to any relative path before storing paths
|
||||
// in MemoFS. The current implementation will error if no prepended absolute path
|
||||
// is found. It really only normalizes paths within the provided path's context.
|
||||
// Example: "/Users/username/project/../other/file.txt" ->
|
||||
// "/Users/username/other/file.txt"
|
||||
// Erroneous example: "/Users/../../other/file.txt" -> "/other/file.txt"
|
||||
// This is not very robust. We should implement proper path normalization here or otherwise
|
||||
// warn if we are missing context and can not fully canonicalize the path correctly.
|
||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
|
||||
let mut normalized = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
std::path::Component::ParentDir => {
|
||||
normalized.pop();
|
||||
}
|
||||
std::path::Component::CurDir => {}
|
||||
_ => normalized.push(component),
|
||||
}
|
||||
}
|
||||
|
||||
let inner = self.inner.lock().unwrap();
|
||||
match inner.entries.get(&normalized) {
|
||||
Some(_) => Ok(normalized),
|
||||
None => not_found(&normalized),
|
||||
}
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
let inner = self.inner.lock().unwrap();
|
||||
|
||||
|
||||
@@ -70,10 +70,14 @@ 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<()>;
|
||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf>;
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
|
||||
fn watch(&mut self, path: &Path) -> io::Result<()>;
|
||||
@@ -173,6 +177,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 +199,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);
|
||||
@@ -207,6 +226,11 @@ impl VfsInner {
|
||||
self.backend.metadata(path)
|
||||
}
|
||||
|
||||
fn canonicalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
self.backend.canonicalize(path)
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
self.backend.event_receiver()
|
||||
}
|
||||
@@ -326,6 +350,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].
|
||||
@@ -359,6 +419,19 @@ impl Vfs {
|
||||
self.inner.lock().unwrap().metadata(path)
|
||||
}
|
||||
|
||||
/// Normalize a path via the underlying backend.
|
||||
///
|
||||
/// Roughly equivalent to [`std::fs::canonicalize`][std::fs::canonicalize]. Relative paths are
|
||||
/// resolved against the backend's current working directory (if applicable) and errors are
|
||||
/// surfaced directly from the backend.
|
||||
///
|
||||
/// [std::fs::canonicalize]: https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html
|
||||
#[inline]
|
||||
pub fn canonicalize<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
self.inner.lock().unwrap().canonicalize(path)
|
||||
}
|
||||
|
||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||
#[inline]
|
||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
@@ -428,6 +501,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].
|
||||
@@ -461,6 +559,13 @@ impl VfsLock<'_> {
|
||||
self.inner.metadata(path)
|
||||
}
|
||||
|
||||
/// Normalize a path via the underlying backend.
|
||||
#[inline]
|
||||
pub fn normalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
|
||||
let path = path.as_ref();
|
||||
self.inner.canonicalize(path)
|
||||
}
|
||||
|
||||
/// Retrieve a handle to the event receiver for this `Vfs`.
|
||||
#[inline]
|
||||
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
@@ -476,7 +581,9 @@ impl VfsLock<'_> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{InMemoryFs, Vfs, VfsSnapshot};
|
||||
use crate::{InMemoryFs, StdBackend, Vfs, VfsSnapshot};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// https://github.com/rojo-rbx/rojo/issues/899
|
||||
#[test]
|
||||
@@ -492,4 +599,62 @@ mod test {
|
||||
"bar\nfoo\n\n"
|
||||
);
|
||||
}
|
||||
|
||||
/// https://github.com/rojo-rbx/rojo/issues/1200
|
||||
#[test]
|
||||
fn canonicalize_in_memory_success() {
|
||||
let mut imfs = InMemoryFs::new();
|
||||
let contents = "Lorem ipsum dolor sit amet.".to_string();
|
||||
|
||||
imfs.load_snapshot("/test/file.txt", VfsSnapshot::file(contents.to_string()))
|
||||
.unwrap();
|
||||
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
assert_eq!(
|
||||
vfs.canonicalize("/test/nested/../file.txt").unwrap(),
|
||||
PathBuf::from("/test/file.txt")
|
||||
);
|
||||
assert_eq!(
|
||||
vfs.read_to_string(vfs.canonicalize("/test/nested/../file.txt").unwrap())
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
contents.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_in_memory_missing_errors() {
|
||||
let imfs = InMemoryFs::new();
|
||||
let vfs = Vfs::new(imfs);
|
||||
|
||||
let err = vfs.canonicalize("test").unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_std_backend_success() {
|
||||
let contents = "Lorem ipsum dolor sit amet.".to_string();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file_path = dir.path().join("file.txt");
|
||||
fs_err::write(&file_path, contents.to_string()).unwrap();
|
||||
|
||||
let vfs = Vfs::new(StdBackend::new());
|
||||
let canonicalized = vfs.canonicalize(&file_path).unwrap();
|
||||
assert_eq!(canonicalized, file_path.canonicalize().unwrap());
|
||||
assert_eq!(
|
||||
vfs.read_to_string(&canonicalized).unwrap().to_string(),
|
||||
contents.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalize_std_backend_missing_errors() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file_path = dir.path().join("test");
|
||||
|
||||
let vfs = Vfs::new(StdBackend::new());
|
||||
let err = vfs.canonicalize(&file_path).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -38,6 +50,10 @@ impl VfsBackend for NoopBackend {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn canonicalize(&mut self, _path: &Path) -> io::Result<PathBuf> {
|
||||
Err(io::Error::other("NoopBackend doesn't do anything"))
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
crossbeam_channel::never()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -94,6 +106,10 @@ impl VfsBackend for StdBackend {
|
||||
})
|
||||
}
|
||||
|
||||
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
|
||||
fs_err::canonicalize(path)
|
||||
}
|
||||
|
||||
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
|
||||
self.watcher_receiver.clone()
|
||||
}
|
||||
|
||||
Submodule plugin/Packages/Highlighter updated: e0d061449e...c12c488dad
@@ -1 +1 @@
|
||||
7.6.1
|
||||
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
|
||||
|
||||
@@ -255,31 +290,39 @@ function ApiContext:open(id)
|
||||
end
|
||||
|
||||
function ApiContext:serialize(ids: { string })
|
||||
local url = ("%s/api/serialize/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||
local url = ("%s/api/serialize"):format(self.__baseUrl)
|
||||
local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids })
|
||||
|
||||
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||
if body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
return Http.post(url, request_body)
|
||||
:andThen(rejectFailedRequests)
|
||||
:andThen(Http.Response.json)
|
||||
:andThen(function(response_body)
|
||||
if response_body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
|
||||
assert(validateApiSerialize(body))
|
||||
assert(validateApiSerialize(response_body))
|
||||
|
||||
return body
|
||||
end)
|
||||
return response_body
|
||||
end)
|
||||
end
|
||||
|
||||
function ApiContext:refPatch(ids: { string })
|
||||
local url = ("%s/api/ref-patch/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||
local url = ("%s/api/ref-patch"):format(self.__baseUrl)
|
||||
local request_body = Http.jsonEncode({ sessionId = self.__sessionId, ids = ids })
|
||||
|
||||
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||
if body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
return Http.post(url, request_body)
|
||||
:andThen(rejectFailedRequests)
|
||||
:andThen(Http.Response.json)
|
||||
:andThen(function(response_body)
|
||||
if response_body.sessionId ~= self.__sessionId then
|
||||
return Promise.reject("Server changed ID")
|
||||
end
|
||||
|
||||
assert(validateApiRefPatch(body))
|
||||
assert(validateApiRefPatch(response_body))
|
||||
|
||||
return body
|
||||
end)
|
||||
return response_body
|
||||
end)
|
||||
end
|
||||
|
||||
return ApiContext
|
||||
|
||||
@@ -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 } }
|
||||
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
|
||||
-- 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
|
||||
return diffs
|
||||
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
|
||||
|
||||
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
|
||||
-- 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")
|
||||
|
||||
return StringDiff._bisect(text1, text2)
|
||||
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 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
|
||||
|
||||
-- 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)
|
||||
|
||||
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
|
||||
end
|
||||
-- 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
|
||||
end
|
||||
end
|
||||
else
|
||||
if string.match(text, "%S") then
|
||||
remove[oldLineNum] = true
|
||||
end
|
||||
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
|
||||
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,43 +254,196 @@ function StringDiffVisualizer:render()
|
||||
CornerRadius = UDim.new(0, 5),
|
||||
}),
|
||||
}),
|
||||
Separator = e("Frame", {
|
||||
Size = UDim2.new(0, 2, 1, 0),
|
||||
Position = UDim2.new(0.5, 0, 0, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
BorderSizePixel = 0,
|
||||
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,
|
||||
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,
|
||||
}, {
|
||||
Source = e(CodeLabel, {
|
||||
size = UDim2.new(1, 0, 1, 0),
|
||||
Separator = e("Frame", {
|
||||
Size = UDim2.new(0, 2, 1, 0),
|
||||
Position = UDim2.new(0.5, 0, 0, 0),
|
||||
AnchorPoint = Vector2.new(0.5, 0),
|
||||
BorderSizePixel = 0,
|
||||
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||
BackgroundTransparency = 0.5,
|
||||
}),
|
||||
Current = e(VirtualScroller, {
|
||||
position = UDim2.new(0, 0, 0, 0),
|
||||
text = oldString,
|
||||
lineBackground = theme.Diff.Background.Remove,
|
||||
markedLines = self.state.remove,
|
||||
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 = 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,
|
||||
}, {
|
||||
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,
|
||||
}),
|
||||
}),
|
||||
New = e(ScrollingFrame, {
|
||||
position = UDim2.new(0.5, 5, 0, 2),
|
||||
size = UDim2.new(0.5, -7, 1, -4),
|
||||
scrollingDirection = Enum.ScrollingDirection.XY,
|
||||
transparency = self.props.transparency,
|
||||
contentSize = self.contentSize,
|
||||
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,
|
||||
}, {
|
||||
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,
|
||||
}),
|
||||
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,
|
||||
}, {
|
||||
|
||||
@@ -23,8 +23,8 @@ function ConfirmingPage:init()
|
||||
|
||||
self:setState({
|
||||
showingStringDiff = false,
|
||||
oldString = "",
|
||||
newString = "",
|
||||
currentString = "",
|
||||
incomingString = "",
|
||||
showingTableDiff = false,
|
||||
oldTable = {},
|
||||
newTable = {},
|
||||
@@ -56,11 +56,11 @@ function ConfirmingPage:render()
|
||||
|
||||
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? })
|
||||
@@ -167,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,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -201,7 +201,20 @@ function ServeSession:start()
|
||||
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)
|
||||
@@ -536,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
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
source: tests/tests/build.rs
|
||||
expression: contents
|
||||
---
|
||||
<roblox version="4">
|
||||
<Item class="Folder" referent="0">
|
||||
<Properties>
|
||||
<string name="Name">json_model_legacy_name</string>
|
||||
</Properties>
|
||||
<Item class="Folder" referent="1">
|
||||
<Properties>
|
||||
<string name="Name">Expected Name</string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
</roblox>
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: tests/tests/build.rs
|
||||
assertion_line: 109
|
||||
expression: contents
|
||||
---
|
||||
<roblox version="4">
|
||||
<Item class="DataModel" referent="0">
|
||||
<Properties>
|
||||
<string name="Name">model_json_name_input</string>
|
||||
</Properties>
|
||||
<Item class="Workspace" referent="1">
|
||||
<Properties>
|
||||
<string name="Name">Workspace</string>
|
||||
<bool name="NeedsPivotMigration">false</bool>
|
||||
</Properties>
|
||||
<Item class="StringValue" referent="2">
|
||||
<Properties>
|
||||
<string name="Name">/Bar</string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
</Item>
|
||||
</roblox>
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: tests/tests/build.rs
|
||||
assertion_line: 108
|
||||
expression: contents
|
||||
---
|
||||
<roblox version="4">
|
||||
<Item class="Folder" referent="0">
|
||||
<Properties>
|
||||
<string name="Name">slugified_name_roundtrip</string>
|
||||
</Properties>
|
||||
<Item class="Script" referent="1">
|
||||
<Properties>
|
||||
<string name="Name">/Script</string>
|
||||
<token name="RunContext">0</token>
|
||||
<string name="Source"><![CDATA[print("Hello world!")
|
||||
]]></string>
|
||||
</Properties>
|
||||
</Item>
|
||||
</Item>
|
||||
</roblox>
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "json_model_legacy_name",
|
||||
"tree": {
|
||||
"$path": "folder"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"Name": "Overridden Name",
|
||||
"ClassName": "Folder"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "model_json_name_input",
|
||||
"tree": {
|
||||
"$className": "DataModel",
|
||||
"Workspace": {
|
||||
"$className": "Workspace",
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "/Bar",
|
||||
"className": "StringValue"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "/Script"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
print("Hello world!")
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "slugified_name_roundtrip",
|
||||
"tree": {
|
||||
"$path": "src"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "/Script"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
print("Hello world!")
|
||||
@@ -6,7 +6,7 @@ expectedPlaceIds: ~
|
||||
gameId: ~
|
||||
placeId: ~
|
||||
projectName: add_folder
|
||||
protocolVersion: 4
|
||||
protocolVersion: 5
|
||||
rootInstanceId: id-2
|
||||
serverVersion: "[server-version]"
|
||||
sessionId: id-1
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-3:
|
||||
Children: []
|
||||
ClassName: Folder
|
||||
Id: id-3
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: my-new-folder
|
||||
Parent: id-2
|
||||
Properties: {}
|
||||
removed: []
|
||||
updated: []
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-3:
|
||||
Children: []
|
||||
ClassName: Folder
|
||||
Id: id-3
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: my-new-folder
|
||||
Parent: id-2
|
||||
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,19 +1,19 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Source:
|
||||
String: "-- Edited contents"
|
||||
id: id-2
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
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,21 +1,23 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-3:
|
||||
Children: []
|
||||
ClassName: Model
|
||||
Id: id-3
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: test
|
||||
Parent: id-2
|
||||
Properties:
|
||||
NeedsPivotMigration:
|
||||
Bool: false
|
||||
removed: []
|
||||
updated: []
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-3:
|
||||
Children: []
|
||||
ClassName: Model
|
||||
Id: id-3
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: test
|
||||
Parent: id-2
|
||||
Properties:
|
||||
NeedsPivotMigration:
|
||||
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,27 +1,29 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-6:
|
||||
Children: []
|
||||
ClassName: Actor
|
||||
Id: id-6
|
||||
Metadata:
|
||||
ignoreUnknownInstances: true
|
||||
Name: Actor
|
||||
Parent: id-3
|
||||
Properties:
|
||||
NeedsPivotMigration:
|
||||
Bool: false
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata:
|
||||
ignoreUnknownInstances: true
|
||||
changedName: ~
|
||||
changedProperties: {}
|
||||
id: id-3
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-6:
|
||||
Children: []
|
||||
ClassName: Actor
|
||||
Id: id-6
|
||||
Metadata:
|
||||
ignoreUnknownInstances: true
|
||||
Name: Actor
|
||||
Parent: id-3
|
||||
Properties:
|
||||
NeedsPivotMigration:
|
||||
Bool: false
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata:
|
||||
ignoreUnknownInstances: true
|
||||
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,141 +1,141 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-10:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-10
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "6"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #6"
|
||||
id-11:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-11
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "7"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #7"
|
||||
id-12:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-12
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "8"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #8"
|
||||
id-13:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-13
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "9"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #9"
|
||||
id-3:
|
||||
Children:
|
||||
- id-4
|
||||
- id-5
|
||||
- id-6
|
||||
- id-7
|
||||
- id-8
|
||||
- id-9
|
||||
- id-10
|
||||
- id-11
|
||||
- id-12
|
||||
- id-13
|
||||
ClassName: Folder
|
||||
Id: id-3
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: new-stuff
|
||||
Parent: id-2
|
||||
Properties: {}
|
||||
id-4:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-4
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "0"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #0"
|
||||
id-5:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-5
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "1"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #1"
|
||||
id-6:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-6
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "2"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #2"
|
||||
id-7:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-7
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "3"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #3"
|
||||
id-8:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-8
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "4"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #4"
|
||||
id-9:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-9
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "5"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #5"
|
||||
removed: []
|
||||
updated: []
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-10:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-10
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "6"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #6"
|
||||
id-11:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-11
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "7"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #7"
|
||||
id-12:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-12
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "8"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #8"
|
||||
id-13:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-13
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "9"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #9"
|
||||
id-3:
|
||||
Children:
|
||||
- id-4
|
||||
- id-5
|
||||
- id-6
|
||||
- id-7
|
||||
- id-8
|
||||
- id-9
|
||||
- id-10
|
||||
- id-11
|
||||
- id-12
|
||||
- id-13
|
||||
ClassName: Folder
|
||||
Id: id-3
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: new-stuff
|
||||
Parent: id-2
|
||||
Properties: {}
|
||||
id-4:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-4
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "0"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #0"
|
||||
id-5:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-5
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "1"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #1"
|
||||
id-6:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-6
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "2"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #2"
|
||||
id-7:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-7
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "3"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #3"
|
||||
id-8:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-8
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "4"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
String: "File #4"
|
||||
id-9:
|
||||
Children: []
|
||||
ClassName: StringValue
|
||||
Id: id-9
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: "5"
|
||||
Parent: id-3
|
||||
Properties:
|
||||
Value:
|
||||
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,17 +1,19 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Scale:
|
||||
Float32: 1
|
||||
id: id-8
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
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:
|
||||
- added: {}
|
||||
removed:
|
||||
- id-4
|
||||
updated: []
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed:
|
||||
- id-4
|
||||
updated: []
|
||||
packetType: messages
|
||||
sessionId: id-1
|
||||
|
||||
|
||||
@@ -1,47 +1,49 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-11:
|
||||
Children: []
|
||||
ClassName: Model
|
||||
Id: id-11
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: ProjectPointer
|
||||
Parent: id-7
|
||||
Properties:
|
||||
Attributes:
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added:
|
||||
id-11:
|
||||
Children: []
|
||||
ClassName: Model
|
||||
Id: id-11
|
||||
Metadata:
|
||||
ignoreUnknownInstances: false
|
||||
Name: ProjectPointer
|
||||
Parent: id-7
|
||||
Properties:
|
||||
Attributes:
|
||||
Rojo_Target_PrimaryPart:
|
||||
String: project target
|
||||
NeedsPivotMigration:
|
||||
Bool: false
|
||||
PrimaryPart:
|
||||
Ref: id-9
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata:
|
||||
ignoreUnknownInstances: false
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Attributes:
|
||||
Attributes:
|
||||
Rojo_Target_PrimaryPart:
|
||||
String: project target
|
||||
NeedsPivotMigration:
|
||||
Bool: false
|
||||
PrimaryPart:
|
||||
Ref: id-9
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata:
|
||||
ignoreUnknownInstances: false
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Attributes:
|
||||
Rojo_Id:
|
||||
String: model target 2
|
||||
id: id-7
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Attributes:
|
||||
Attributes:
|
||||
Rojo_Id:
|
||||
String: model target 2
|
||||
id: id-7
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Attributes:
|
||||
Rojo_Target_PrimaryPart:
|
||||
String: model target 2
|
||||
PrimaryPart: ~
|
||||
id: id-8
|
||||
Attributes:
|
||||
Rojo_Target_PrimaryPart:
|
||||
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:
|
||||
- added: {}
|
||||
removed:
|
||||
- id-3
|
||||
updated: []
|
||||
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,19 +1,19 @@
|
||||
---
|
||||
source: tests/tests/serve.rs
|
||||
expression: "subscribe_response.intern_and_redact(&mut redactions, ())"
|
||||
|
||||
expression: "socket_packet.intern_and_redact(&mut redactions, ())"
|
||||
---
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
Source:
|
||||
String: Updated foo!
|
||||
id: id-4
|
||||
body:
|
||||
messageCursor: 1
|
||||
messages:
|
||||
- added: {}
|
||||
removed: []
|
||||
updated:
|
||||
- changedClassName: ~
|
||||
changedMetadata: ~
|
||||
changedName: ~
|
||||
changedProperties:
|
||||
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
|
||||
assertion_line: 101
|
||||
expression: "String::from_utf8_lossy(&output.stdout)"
|
||||
---
|
||||
|
||||
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user