mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-22 21:41:37 +00:00
Merge branch 'master' of https://github.com/boatbomber/rojo into headless-api
This commit is contained in:
commit
dda01ae9bb
30 changed files with 14454 additions and 5335 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -13,3 +13,6 @@
|
||||||
[submodule "plugin/Packages/TestEZ"]
|
[submodule "plugin/Packages/TestEZ"]
|
||||||
path = plugin/Packages/TestEZ
|
path = plugin/Packages/TestEZ
|
||||||
url = https://github.com/roblox/testez.git
|
url = https://github.com/roblox/testez.git
|
||||||
|
[submodule "plugin/Packages/Highlighter"]
|
||||||
|
path = plugin/Packages/Highlighter
|
||||||
|
url = https://github.com/boatbomber/highlighter.git
|
||||||
|
|
|
||||||
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
## Unreleased Changes
|
## Unreleased Changes
|
||||||
* Added headless API for Studio companion plugins. ([#631])
|
* Added headless API for Studio companion plugins. ([#631])
|
||||||
|
* Internally for themes, we use `Color3.fromHex` now instead of custom-made solution ([#761]).
|
||||||
|
* On failing to open a file from Roblox Studio, it is now logged appropriately instead of being ignored ([#745]).
|
||||||
* Significantly improved performance of `rojo sourcemap`. ([#668])
|
* Significantly improved performance of `rojo sourcemap`. ([#668])
|
||||||
* Fixed the diff visualizer of connected sessions. ([#674])
|
* Fixed the diff visualizer of connected sessions. ([#674])
|
||||||
* Fixed disconnected session activity. ([#675])
|
* Fixed disconnected session activity. ([#675])
|
||||||
|
|
@ -20,8 +22,16 @@
|
||||||
* Added support for syncing in `.toml` files ([#633])
|
* Added support for syncing in `.toml` files ([#633])
|
||||||
* Add `plugin` flag to the `build` command that outputs to the local plugins folder ([#735])
|
* Add `plugin` flag to the `build` command that outputs to the local plugins folder ([#735])
|
||||||
* Added better support for `Font` properties ([#731])
|
* Added better support for `Font` properties ([#731])
|
||||||
|
* Add new plugin template to the `init` command ([#738])
|
||||||
|
* Added rich Source diffs in patch visualizer ([#748])
|
||||||
|
* Fix PatchTree performance issues ([#755])
|
||||||
|
* Don't override the initial enabled state for source diffing ([#760])
|
||||||
|
* Added support for `Terrain.MaterialColors` ([#770])
|
||||||
|
* Allow `Terrain` to be specified without a classname ([#771])
|
||||||
|
|
||||||
[#631]: https://github.com/rojo-rbx/rojo/pull/631
|
[#631]: https://github.com/rojo-rbx/rojo/pull/631
|
||||||
|
[#761]: https://github.com/rojo-rbx/rojo/pull/761
|
||||||
|
[#745]: https://github.com/rojo-rbx/rojo/pull/745
|
||||||
[#668]: https://github.com/rojo-rbx/rojo/pull/668
|
[#668]: https://github.com/rojo-rbx/rojo/pull/668
|
||||||
[#674]: https://github.com/rojo-rbx/rojo/pull/674
|
[#674]: https://github.com/rojo-rbx/rojo/pull/674
|
||||||
[#675]: https://github.com/rojo-rbx/rojo/pull/675
|
[#675]: https://github.com/rojo-rbx/rojo/pull/675
|
||||||
|
|
@ -40,6 +50,12 @@
|
||||||
[#633]: https://github.com/rojo-rbx/rojo/pull/633
|
[#633]: https://github.com/rojo-rbx/rojo/pull/633
|
||||||
[#735]: https://github.com/rojo-rbx/rojo/pull/735
|
[#735]: https://github.com/rojo-rbx/rojo/pull/735
|
||||||
[#731]: https://github.com/rojo-rbx/rojo/pull/731
|
[#731]: https://github.com/rojo-rbx/rojo/pull/731
|
||||||
|
[#738]: https://github.com/rojo-rbx/rojo/pull/738
|
||||||
|
[#748]: https://github.com/rojo-rbx/rojo/pull/748
|
||||||
|
[#755]: https://github.com/rojo-rbx/rojo/pull/755
|
||||||
|
[#760]: https://github.com/rojo-rbx/rojo/pull/760
|
||||||
|
[#770]: https://github.com/rojo-rbx/rojo/pull/770
|
||||||
|
[#771]: https://github.com/rojo-rbx/rojo/pull/771
|
||||||
|
|
||||||
## [7.3.0] - April 22, 2023
|
## [7.3.0] - April 22, 2023
|
||||||
* Added `$attributes` to project format. ([#574])
|
* Added `$attributes` to project format. ([#574])
|
||||||
|
|
|
||||||
29
Cargo.lock
generated
29
Cargo.lock
generated
|
|
@ -1647,9 +1647,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_binary"
|
name = "rbx_binary"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1631584eb343930a5a738635504b8c1f3ef2ae9c947bff942bbbac075f6887f6"
|
checksum = "4e50573021d04b680018955662eba7dc4aac3de92219231798f6c9b41e38ab01"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"lz4",
|
"lz4",
|
||||||
|
|
@ -1662,9 +1662,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_dom_weak"
|
name = "rbx_dom_weak"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7cc2238fd858d706f4f5c6a0f9af3cd4a10f3f8dc8b6534e683f8543af26f33"
|
checksum = "161729449bbb0cfa657ce7bcca6a160d0af06d8b8d9efdc9abe14735dccacdb9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rbx_types",
|
"rbx_types",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -1672,19 +1672,20 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_reflection"
|
name = "rbx_reflection"
|
||||||
version = "4.2.0"
|
version = "4.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8915748c8a3b2a92540e4e35e99ebd548df2d62b0a6cf38ae5d0081f0e611d5"
|
checksum = "08bd48487192046fec8f805f3fa29f3d7d5beb9890b0859b1a92bd8aff580343"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rbx_types",
|
"rbx_types",
|
||||||
"serde",
|
"serde",
|
||||||
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_reflection_database"
|
name = "rbx_reflection_database"
|
||||||
version = "0.2.6+roblox-572"
|
version = "0.2.7+roblox-588"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "780a70e2aff0c2a922bf2e72449e3a67e656ce622af4f8895c22d15cd98dfd5b"
|
checksum = "1be6cf674182806f11ad4899dd1feafe977591f1ae035ae05a58d4b74e487276"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"rbx_reflection",
|
"rbx_reflection",
|
||||||
|
|
@ -1694,9 +1695,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_types"
|
name = "rbx_types"
|
||||||
version = "1.5.0"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9644faf3ae1858ba10615fa5955c330e49cf0f6ad37a83db3e1c398c4afac12"
|
checksum = "070106e926b8ae54c7bc443e5db4d868d7f0af51c1d7cfd7efe1364c1753d8a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
|
@ -1709,9 +1710,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rbx_xml"
|
name = "rbx_xml"
|
||||||
version = "0.13.0"
|
version = "0.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d5f9361d83d06aae8ea42d7002d35e2652b198e8ce68c918b97e371f8a31352"
|
checksum = "4bc65b70827519fdc4ab47416d1085b912f087fadab9ed415471b6daba635574"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -1856,9 +1857,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rmp-serde"
|
name = "rmp-serde"
|
||||||
version = "0.14.4"
|
version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ce7d70c926fe472aed493b902010bccc17fa9f7284145cb8772fd22fdb052d8"
|
checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"rmp",
|
"rmp",
|
||||||
|
|
|
||||||
20
Cargo.toml
20
Cargo.toml
|
|
@ -12,9 +12,7 @@ readme = "README.md"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
exclude = [
|
exclude = ["/test-projects/**"]
|
||||||
"/test-projects/**",
|
|
||||||
]
|
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
@ -51,11 +49,11 @@ memofs = { version = "0.2.0", path = "crates/memofs" }
|
||||||
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
|
||||||
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
# rbx_xml = { path = "../rbx-dom/rbx_xml" }
|
||||||
|
|
||||||
rbx_binary = "0.7.0"
|
rbx_binary = "0.7.1"
|
||||||
rbx_dom_weak = "2.4.0"
|
rbx_dom_weak = "2.5.0"
|
||||||
rbx_reflection = "4.2.0"
|
rbx_reflection = "4.3.0"
|
||||||
rbx_reflection_database = "0.2.6"
|
rbx_reflection_database = "0.2.7"
|
||||||
rbx_xml = "0.13.0"
|
rbx_xml = "0.13.1"
|
||||||
|
|
||||||
anyhow = "1.0.44"
|
anyhow = "1.0.44"
|
||||||
backtrace = "0.3.61"
|
backtrace = "0.3.61"
|
||||||
|
|
@ -75,7 +73,11 @@ notify = "4.0.17"
|
||||||
num_cpus = "1.15.0"
|
num_cpus = "1.15.0"
|
||||||
opener = "0.5.0"
|
opener = "0.5.0"
|
||||||
rayon = "1.7.0"
|
rayon = "1.7.0"
|
||||||
reqwest = { version = "0.11.10", features = ["blocking", "json", "native-tls-vendored"] }
|
reqwest = { version = "0.11.10", features = [
|
||||||
|
"blocking",
|
||||||
|
"json",
|
||||||
|
"native-tls-vendored",
|
||||||
|
] }
|
||||||
ritz = "0.1.0"
|
ritz = "0.1.0"
|
||||||
roblox_install = "1.0.0"
|
roblox_install = "1.0.0"
|
||||||
serde = { version = "1.0.130", features = ["derive", "rc"] }
|
serde = { version = "1.0.130", features = ["derive", "rc"] }
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
|
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
To build this library or plugin, use:
|
To build this library, use:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rojo build -o "{project_name}.rbxmx"
|
rojo build -o "{project_name}.rbxmx"
|
||||||
|
|
|
||||||
17
assets/default-plugin-project/README.md
Normal file
17
assets/default-plugin-project/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# {project_name}
|
||||||
|
Generated by [Rojo](https://github.com/rojo-rbx/rojo) {rojo_version}.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
To build this plugin to your local plugins folder, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rojo build -p "{project_name}.rbxm"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can include the `watch` flag to re-build it on save:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rojo build -p "{project_name}.rbxm" --watch
|
||||||
|
```
|
||||||
|
|
||||||
|
For more help, check out [the Rojo documentation](https://rojo.space/docs).
|
||||||
6
assets/default-plugin-project/default.project.json
Normal file
6
assets/default-plugin-project/default.project.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "{project_name}",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
assets/default-plugin-project/gitignore.txt
Normal file
3
assets/default-plugin-project/gitignore.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Plugin model files
|
||||||
|
/{project_name}.rbxmx
|
||||||
|
/{project_name}.rbxm
|
||||||
1
plugin/Packages/Highlighter
Submodule
1
plugin/Packages/Highlighter
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit e0d061449ea5c4452ef77008b5197ae4d3d77621
|
||||||
|
|
@ -265,6 +265,26 @@ types = {
|
||||||
toPod = identity,
|
toPod = identity,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
MaterialColors = {
|
||||||
|
fromPod = function(pod: { [string]: { number } })
|
||||||
|
local real = {}
|
||||||
|
for name, color in pod do
|
||||||
|
real[Enum.Material[name]] = Color3.fromRGB(color[1], color[2], color[3])
|
||||||
|
end
|
||||||
|
return real
|
||||||
|
end,
|
||||||
|
toPod = function(roblox: { [Enum.Material]: Color3 })
|
||||||
|
local pod = {}
|
||||||
|
for material, color in roblox do
|
||||||
|
pod[material.Name] = {
|
||||||
|
math.round(math.clamp(color.R, 0, 1) * 255),
|
||||||
|
math.round(math.clamp(color.G, 0, 1) * 255),
|
||||||
|
math.round(math.clamp(color.B, 0, 1) * 255)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
NumberRange = {
|
NumberRange = {
|
||||||
fromPod = unpackDecoder(NumberRange.new),
|
fromPod = unpackDecoder(NumberRange.new),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,118 @@
|
||||||
},
|
},
|
||||||
"ty": "Int64"
|
"ty": "Int64"
|
||||||
},
|
},
|
||||||
|
"MaterialColors": {
|
||||||
|
"value": {
|
||||||
|
"MaterialColors": {
|
||||||
|
"Grass": [
|
||||||
|
106,
|
||||||
|
127,
|
||||||
|
63
|
||||||
|
],
|
||||||
|
"Slate": [
|
||||||
|
63,
|
||||||
|
127,
|
||||||
|
107
|
||||||
|
],
|
||||||
|
"Concrete": [
|
||||||
|
127,
|
||||||
|
102,
|
||||||
|
63
|
||||||
|
],
|
||||||
|
"Brick": [
|
||||||
|
138,
|
||||||
|
86,
|
||||||
|
62
|
||||||
|
],
|
||||||
|
"Sand": [
|
||||||
|
143,
|
||||||
|
126,
|
||||||
|
95
|
||||||
|
],
|
||||||
|
"WoodPlanks": [
|
||||||
|
139,
|
||||||
|
109,
|
||||||
|
79
|
||||||
|
],
|
||||||
|
"Rock": [
|
||||||
|
102,
|
||||||
|
108,
|
||||||
|
111
|
||||||
|
],
|
||||||
|
"Glacier": [
|
||||||
|
101,
|
||||||
|
176,
|
||||||
|
234
|
||||||
|
],
|
||||||
|
"Snow": [
|
||||||
|
195,
|
||||||
|
199,
|
||||||
|
218
|
||||||
|
],
|
||||||
|
"Sandstone": [
|
||||||
|
137,
|
||||||
|
90,
|
||||||
|
71
|
||||||
|
],
|
||||||
|
"Mud": [
|
||||||
|
58,
|
||||||
|
46,
|
||||||
|
36
|
||||||
|
],
|
||||||
|
"Basalt": [
|
||||||
|
30,
|
||||||
|
30,
|
||||||
|
37
|
||||||
|
],
|
||||||
|
"Ground": [
|
||||||
|
102,
|
||||||
|
92,
|
||||||
|
59
|
||||||
|
],
|
||||||
|
"CrackedLava": [
|
||||||
|
232,
|
||||||
|
156,
|
||||||
|
74
|
||||||
|
],
|
||||||
|
"Asphalt": [
|
||||||
|
115,
|
||||||
|
123,
|
||||||
|
107
|
||||||
|
],
|
||||||
|
"Cobblestone": [
|
||||||
|
132,
|
||||||
|
123,
|
||||||
|
90
|
||||||
|
],
|
||||||
|
"Ice": [
|
||||||
|
129,
|
||||||
|
194,
|
||||||
|
224
|
||||||
|
],
|
||||||
|
"LeafyGrass": [
|
||||||
|
115,
|
||||||
|
132,
|
||||||
|
74
|
||||||
|
],
|
||||||
|
"Salt": [
|
||||||
|
198,
|
||||||
|
189,
|
||||||
|
181
|
||||||
|
],
|
||||||
|
"Limestone": [
|
||||||
|
206,
|
||||||
|
173,
|
||||||
|
148
|
||||||
|
],
|
||||||
|
"Pavement": [
|
||||||
|
148,
|
||||||
|
148,
|
||||||
|
140
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ty": "MaterialColors"
|
||||||
|
},
|
||||||
"NumberRange": {
|
"NumberRange": {
|
||||||
"value": {
|
"value": {
|
||||||
"NumberRange": [
|
"NumberRange": [
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,30 @@
|
||||||
local CollectionService = game:GetService("CollectionService")
|
local CollectionService = game:GetService("CollectionService")
|
||||||
|
|
||||||
|
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors
|
||||||
|
local TERRAIN_MATERIAL_COLORS = {
|
||||||
|
Enum.Material.Grass,
|
||||||
|
Enum.Material.Slate,
|
||||||
|
Enum.Material.Concrete,
|
||||||
|
Enum.Material.Brick,
|
||||||
|
Enum.Material.Sand,
|
||||||
|
Enum.Material.WoodPlanks,
|
||||||
|
Enum.Material.Rock,
|
||||||
|
Enum.Material.Glacier,
|
||||||
|
Enum.Material.Snow,
|
||||||
|
Enum.Material.Sandstone,
|
||||||
|
Enum.Material.Mud,
|
||||||
|
Enum.Material.Basalt,
|
||||||
|
Enum.Material.Ground,
|
||||||
|
Enum.Material.CrackedLava,
|
||||||
|
Enum.Material.Asphalt,
|
||||||
|
Enum.Material.Cobblestone,
|
||||||
|
Enum.Material.Ice,
|
||||||
|
Enum.Material.LeafyGrass,
|
||||||
|
Enum.Material.Salt,
|
||||||
|
Enum.Material.Limestone,
|
||||||
|
Enum.Material.Pavement,
|
||||||
|
}
|
||||||
|
|
||||||
-- Defines how to read and write properties that aren't directly scriptable.
|
-- Defines how to read and write properties that aren't directly scriptable.
|
||||||
--
|
--
|
||||||
-- The reflection database refers to these as having scriptability = "Custom"
|
-- The reflection database refers to these as having scriptability = "Custom"
|
||||||
|
|
@ -71,4 +96,24 @@ return {
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Terrain = {
|
||||||
|
MaterialColors = {
|
||||||
|
read = function(instance: Terrain)
|
||||||
|
-- There's no way to get a list of every color, so we have to
|
||||||
|
-- make one.
|
||||||
|
local colors = {}
|
||||||
|
for _, material in TERRAIN_MATERIAL_COLORS do
|
||||||
|
colors[material] = instance:GetMaterialColor(material)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, colors
|
||||||
|
end,
|
||||||
|
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
|
||||||
|
for material, color in value do
|
||||||
|
instance:SetMaterialColor(material, color)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
61
plugin/src/App/Components/CodeLabel.lua
Normal file
61
plugin/src/App/Components/CodeLabel.lua
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
local Highlighter = require(Packages.Highlighter)
|
||||||
|
Highlighter.matchStudioSettings()
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
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 e("TextLabel", {
|
||||||
|
Size = self.props.size,
|
||||||
|
Position = self.props.position,
|
||||||
|
Text = self.props.text,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.RobotoMono,
|
||||||
|
TextSize = 16,
|
||||||
|
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
|
||||||
|
|
||||||
|
return CodeLabel
|
||||||
|
|
@ -4,8 +4,10 @@ local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Packages.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
local DisplayValue = require(script.Parent.DisplayValue)
|
local DisplayValue = require(script.Parent.DisplayValue)
|
||||||
|
|
||||||
local EMPTY_TABLE = {}
|
local EMPTY_TABLE = {}
|
||||||
|
|
@ -93,6 +95,89 @@ function ChangeList:render()
|
||||||
local metadata = values[4] or EMPTY_TABLE
|
local metadata = values[4] or EMPTY_TABLE
|
||||||
local isWarning = metadata.isWarning
|
local isWarning = metadata.isWarning
|
||||||
|
|
||||||
|
-- Special case for .Source updates
|
||||||
|
-- because we want to display a syntax highlighted diff for better UX
|
||||||
|
if self.props.showSourceDiff and tostring(values[1]) == "Source" then
|
||||||
|
rows[row] = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 30),
|
||||||
|
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
|
||||||
|
BackgroundColor3 = theme.Diff.Row,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
LayoutOrder = row,
|
||||||
|
}, {
|
||||||
|
Padding = e("UIPadding", pad),
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
}),
|
||||||
|
A = e("TextLabel", {
|
||||||
|
Text = (if isWarning then "⚠ " else "") .. tostring(values[1]),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
Size = UDim2.new(0.3, 0, 1, 0),
|
||||||
|
LayoutOrder = 1,
|
||||||
|
}),
|
||||||
|
Button = e("TextButton", {
|
||||||
|
Text = "",
|
||||||
|
Size = UDim2.new(0.7, 0, 1, -4),
|
||||||
|
LayoutOrder = 2,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
[Roact.Event.Activated] = function()
|
||||||
|
if props.showSourceDiff then
|
||||||
|
props.showSourceDiff(tostring(values[2]), tostring(values[3]))
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
e(BorderedContainer, {
|
||||||
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
transparency = self.props.transparency:map(function(t)
|
||||||
|
return 0.5 + (0.5 * t)
|
||||||
|
end),
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
Padding = UDim.new(0, 5),
|
||||||
|
}),
|
||||||
|
Label = e("TextLabel", {
|
||||||
|
Text = "View Diff",
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
Size = UDim2.new(0, 65, 1, 0),
|
||||||
|
LayoutOrder = 1,
|
||||||
|
}),
|
||||||
|
Icon = e("ImageLabel", {
|
||||||
|
Image = Assets.Images.Icons.Expand,
|
||||||
|
ImageColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
ImageTransparency = self.props.transparency,
|
||||||
|
|
||||||
|
Size = UDim2.new(0, 16, 0, 16),
|
||||||
|
Position = UDim2.new(0.5, 0, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||||
|
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
LayoutOrder = 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
rows[row] = e("Frame", {
|
rows[row] = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 0, 30),
|
Size = UDim2.new(1, 0, 0, 30),
|
||||||
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
|
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ function Expansion:render()
|
||||||
ChangeList = e(ChangeList, {
|
ChangeList = e(ChangeList, {
|
||||||
changes = props.changeList,
|
changes = props.changeList,
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
|
showSourceDiff = props.showSourceDiff,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
@ -170,6 +171,7 @@ function DomLabel:render()
|
||||||
indent = indent,
|
indent = indent,
|
||||||
transparency = props.transparency,
|
transparency = props.transparency,
|
||||||
changeList = props.changeList,
|
changeList = props.changeList,
|
||||||
|
showSourceDiff = props.showSourceDiff,
|
||||||
})
|
})
|
||||||
else nil,
|
else nil,
|
||||||
DiffIcon = if props.patchType
|
DiffIcon = if props.patchType
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ function PatchVisualizer:render()
|
||||||
changeList = node.changeList,
|
changeList = node.changeList,
|
||||||
depth = depth,
|
depth = depth,
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
showSourceDiff = self.props.showSourceDiff,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -23,20 +23,27 @@ local function ScrollingFrame(props)
|
||||||
BottomImage = Assets.Images.ScrollBar.Bottom,
|
BottomImage = Assets.Images.ScrollBar.Bottom,
|
||||||
|
|
||||||
ElasticBehavior = Enum.ElasticBehavior.Always,
|
ElasticBehavior = Enum.ElasticBehavior.Always,
|
||||||
ScrollingDirection = Enum.ScrollingDirection.Y,
|
ScrollingDirection = props.scrollingDirection or Enum.ScrollingDirection.Y,
|
||||||
|
|
||||||
LayoutOrder = props.layoutOrder,
|
LayoutOrder = props.layoutOrder,
|
||||||
Size = props.size,
|
Size = props.size,
|
||||||
Position = props.position,
|
Position = props.position,
|
||||||
AnchorPoint = props.anchorPoint,
|
AnchorPoint = props.anchorPoint,
|
||||||
CanvasSize = props.contentSize:map(function(value)
|
CanvasSize = props.contentSize:map(function(value)
|
||||||
return UDim2.new(0, 0, 0, value.Y)
|
return UDim2.new(
|
||||||
|
0,
|
||||||
|
if (props.scrollingDirection and props.scrollingDirection ~= Enum.ScrollingDirection.Y)
|
||||||
|
then value.X
|
||||||
|
else 0,
|
||||||
|
0,
|
||||||
|
value.Y
|
||||||
|
)
|
||||||
end),
|
end),
|
||||||
|
|
||||||
BorderSizePixel = 0,
|
BorderSizePixel = 0,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
[Roact.Change.AbsoluteSize] = props[Roact.Change.AbsoluteSize]
|
[Roact.Change.AbsoluteSize] = props[Roact.Change.AbsoluteSize],
|
||||||
}, props[Roact.Children])
|
}, props[Roact.Children])
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
441
plugin/src/App/Components/StringDiffVisualizer/StringDiff.lua
Normal file
441
plugin/src/App/Components/StringDiffVisualizer/StringDiff.lua
Normal file
|
|
@ -0,0 +1,441 @@
|
||||||
|
--[[
|
||||||
|
Based on DiffMatchPatch by Neil Fraser.
|
||||||
|
https://github.com/google/diff-match-patch
|
||||||
|
]]
|
||||||
|
|
||||||
|
export type DiffAction = number
|
||||||
|
export type Diff = { actionType: DiffAction, value: string }
|
||||||
|
export type Diffs = { Diff }
|
||||||
|
|
||||||
|
local StringDiff = {
|
||||||
|
ActionTypes = table.freeze({
|
||||||
|
Equal = 0,
|
||||||
|
Delete = 1,
|
||||||
|
Insert = 2,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
function StringDiff.findDiffs(text1: string, text2: string): Diffs
|
||||||
|
-- Validate inputs
|
||||||
|
if type(text1) ~= "string" or type(text2) ~= "string" then
|
||||||
|
error(
|
||||||
|
string.format(
|
||||||
|
"Invalid inputs to StringDiff.findDiffs, expected strings and got (%s, %s)",
|
||||||
|
type(text1),
|
||||||
|
type(text2)
|
||||||
|
),
|
||||||
|
2
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Shortcut if the texts are identical
|
||||||
|
if text1 == text2 then
|
||||||
|
return { { actionType = StringDiff.ActionTypes.Equal, value = text1 } }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Trim off any shared prefix and suffix
|
||||||
|
-- These are easy to detect and can be dealt with quickly without needing a complex diff
|
||||||
|
-- and later we simply add them as Equal to the start and end of the diff
|
||||||
|
local sharedPrefix, sharedSuffix
|
||||||
|
local prefixLength = StringDiff._sharedPrefix(text1, text2)
|
||||||
|
if prefixLength > 0 then
|
||||||
|
-- Store the prefix
|
||||||
|
sharedPrefix = string.sub(text1, 1, prefixLength)
|
||||||
|
-- Now trim it off
|
||||||
|
text1 = string.sub(text1, prefixLength + 1)
|
||||||
|
text2 = string.sub(text2, prefixLength + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local suffixLength = StringDiff._sharedSuffix(text1, text2)
|
||||||
|
if suffixLength > 0 then
|
||||||
|
-- Store the suffix
|
||||||
|
sharedSuffix = string.sub(text1, -suffixLength)
|
||||||
|
-- Now trim it off
|
||||||
|
text1 = string.sub(text1, 1, -suffixLength - 1)
|
||||||
|
text2 = string.sub(text2, 1, -suffixLength - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Compute the diff on the middle block where the changes lie
|
||||||
|
local diffs = StringDiff._computeDiff(text1, text2)
|
||||||
|
|
||||||
|
-- Restore the prefix and suffix
|
||||||
|
if sharedPrefix then
|
||||||
|
table.insert(diffs, 1, { actionType = StringDiff.ActionTypes.Equal, value = sharedPrefix })
|
||||||
|
end
|
||||||
|
if sharedSuffix then
|
||||||
|
table.insert(diffs, { actionType = StringDiff.ActionTypes.Equal, value = sharedSuffix })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Cleanup the diff
|
||||||
|
diffs = StringDiff._reorderAndMerge(diffs)
|
||||||
|
|
||||||
|
return diffs
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._sharedPrefix(text1: string, text2: string): number
|
||||||
|
-- Uses a binary search to find the largest common prefix between the two strings
|
||||||
|
-- Performance analysis: http://neil.fraser.name/news/2007/10/09/
|
||||||
|
|
||||||
|
-- Shortcut common cases
|
||||||
|
if (#text1 == 0) or (#text2 == 0) or (string.byte(text1, 1) ~= string.byte(text2, 1)) then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local pointerMin = 1
|
||||||
|
local pointerMax = math.min(#text1, #text2)
|
||||||
|
local pointerMid = pointerMax
|
||||||
|
local pointerStart = 1
|
||||||
|
while pointerMin < pointerMid do
|
||||||
|
if string.sub(text1, pointerStart, pointerMid) == string.sub(text2, pointerStart, pointerMid) then
|
||||||
|
pointerMin = pointerMid
|
||||||
|
pointerStart = pointerMin
|
||||||
|
else
|
||||||
|
pointerMax = pointerMid
|
||||||
|
end
|
||||||
|
pointerMid = math.floor(pointerMin + (pointerMax - pointerMin) / 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return pointerMid
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._sharedSuffix(text1: string, text2: string): number
|
||||||
|
-- Uses a binary search to find the largest common suffix between the two strings
|
||||||
|
-- Performance analysis: http://neil.fraser.name/news/2007/10/09/
|
||||||
|
|
||||||
|
-- Shortcut common cases
|
||||||
|
if (#text1 == 0) or (#text2 == 0) or (string.byte(text1, -1) ~= string.byte(text2, -1)) then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local pointerMin = 1
|
||||||
|
local pointerMax = math.min(#text1, #text2)
|
||||||
|
local pointerMid = pointerMax
|
||||||
|
local pointerEnd = 1
|
||||||
|
while pointerMin < pointerMid do
|
||||||
|
if string.sub(text1, -pointerMid, -pointerEnd) == string.sub(text2, -pointerMid, -pointerEnd) then
|
||||||
|
pointerMin = pointerMid
|
||||||
|
pointerEnd = pointerMin
|
||||||
|
else
|
||||||
|
pointerMax = pointerMid
|
||||||
|
end
|
||||||
|
pointerMid = math.floor(pointerMin + (pointerMax - pointerMin) / 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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._bisect(text1: string, text2: string): Diffs
|
||||||
|
-- Find the 'middle snake' of a diff, split the problem in two
|
||||||
|
-- and return the recursively constructed diff
|
||||||
|
-- See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations
|
||||||
|
|
||||||
|
-- Cache the text lengths to prevent multiple calls
|
||||||
|
local text1Length = #text1
|
||||||
|
local text2Length = #text2
|
||||||
|
|
||||||
|
local _sub, _element
|
||||||
|
local maxD = math.ceil((text1Length + text2Length) / 2)
|
||||||
|
local vOffset = maxD
|
||||||
|
local vLength = 2 * maxD
|
||||||
|
local v1 = table.create(vLength)
|
||||||
|
local v2 = table.create(vLength)
|
||||||
|
|
||||||
|
-- Setting all elements to -1 is faster in Lua than mixing integers and nil
|
||||||
|
for x = 0, vLength - 1 do
|
||||||
|
v1[x] = -1
|
||||||
|
v2[x] = -1
|
||||||
|
end
|
||||||
|
v1[vOffset + 1] = 0
|
||||||
|
v2[vOffset + 1] = 0
|
||||||
|
local delta = text1Length - text2Length
|
||||||
|
|
||||||
|
-- If the total number of characters is odd, then
|
||||||
|
-- the front path will collide with the reverse path
|
||||||
|
local front = (delta % 2 ~= 0)
|
||||||
|
|
||||||
|
-- Offsets for start and end of k loop
|
||||||
|
-- Prevents mapping of space beyond the grid
|
||||||
|
local k1Start = 0
|
||||||
|
local k1End = 0
|
||||||
|
local k2Start = 0
|
||||||
|
local k2End = 0
|
||||||
|
for d = 0, maxD - 1 do
|
||||||
|
-- Walk the front path one step
|
||||||
|
for k1 = -d + k1Start, d - k1End, 2 do
|
||||||
|
local k1_offset = vOffset + k1
|
||||||
|
local x1
|
||||||
|
if (k1 == -d) or ((k1 ~= d) and (v1[k1_offset - 1] < v1[k1_offset + 1])) then
|
||||||
|
x1 = v1[k1_offset + 1]
|
||||||
|
else
|
||||||
|
x1 = v1[k1_offset - 1] + 1
|
||||||
|
end
|
||||||
|
local y1 = x1 - k1
|
||||||
|
while
|
||||||
|
(x1 <= text1Length)
|
||||||
|
and (y1 <= text2Length)
|
||||||
|
and (string.sub(text1, x1, x1) == string.sub(text2, y1, y1))
|
||||||
|
do
|
||||||
|
x1 = x1 + 1
|
||||||
|
y1 = y1 + 1
|
||||||
|
end
|
||||||
|
v1[k1_offset] = x1
|
||||||
|
if x1 > text1Length + 1 then
|
||||||
|
-- Ran off the right of the graph
|
||||||
|
k1End = k1End + 2
|
||||||
|
elseif y1 > text2Length + 1 then
|
||||||
|
-- Ran off the bottom of the graph
|
||||||
|
k1Start = k1Start + 2
|
||||||
|
elseif front then
|
||||||
|
local k2_offset = vOffset + delta - k1
|
||||||
|
if k2_offset >= 0 and k2_offset < vLength and v2[k2_offset] ~= -1 then
|
||||||
|
-- Mirror x2 onto top-left coordinate system
|
||||||
|
local x2 = text1Length - v2[k2_offset] + 1
|
||||||
|
if x1 > x2 then
|
||||||
|
-- Overlap detected
|
||||||
|
return StringDiff._bisectSplit(text1, text2, x1, y1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Walk the reverse path one step
|
||||||
|
for k2 = -d + k2Start, d - k2End, 2 do
|
||||||
|
local k2_offset = vOffset + k2
|
||||||
|
local x2
|
||||||
|
if (k2 == -d) or ((k2 ~= d) and (v2[k2_offset - 1] < v2[k2_offset + 1])) then
|
||||||
|
x2 = v2[k2_offset + 1]
|
||||||
|
else
|
||||||
|
x2 = v2[k2_offset - 1] + 1
|
||||||
|
end
|
||||||
|
local y2 = x2 - k2
|
||||||
|
while
|
||||||
|
(x2 <= text1Length)
|
||||||
|
and (y2 <= text2Length)
|
||||||
|
and (string.sub(text1, -x2, -x2) == string.sub(text2, -y2, -y2))
|
||||||
|
do
|
||||||
|
x2 = x2 + 1
|
||||||
|
y2 = y2 + 1
|
||||||
|
end
|
||||||
|
v2[k2_offset] = x2
|
||||||
|
if x2 > text1Length + 1 then
|
||||||
|
-- Ran off the left of the graph
|
||||||
|
k2End = k2End + 2
|
||||||
|
elseif y2 > text2Length + 1 then
|
||||||
|
-- Ran off the top of the graph
|
||||||
|
k2Start = k2Start + 2
|
||||||
|
elseif not front then
|
||||||
|
local k1_offset = vOffset + delta - k2
|
||||||
|
if k1_offset >= 0 and k1_offset < vLength and v1[k1_offset] ~= -1 then
|
||||||
|
local x1 = v1[k1_offset]
|
||||||
|
local y1 = vOffset + x1 - k1_offset
|
||||||
|
-- Mirror x2 onto top-left coordinate system
|
||||||
|
x2 = text1Length - x2 + 1
|
||||||
|
if x1 > x2 then
|
||||||
|
-- Overlap detected
|
||||||
|
return StringDiff._bisectSplit(text1, text2, x1, y1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Number of diffs equals number of characters, no commonality at all
|
||||||
|
return {
|
||||||
|
{ actionType = StringDiff.ActionTypes.Delete, value = text1 },
|
||||||
|
{ actionType = StringDiff.ActionTypes.Insert, value = text2 },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._bisectSplit(text1: string, text2: string, x: number, y: number): Diffs
|
||||||
|
-- Given the location of the 'middle snake',
|
||||||
|
-- split the diff in two parts and recurse
|
||||||
|
|
||||||
|
local text1a = string.sub(text1, 1, x - 1)
|
||||||
|
local text2a = string.sub(text2, 1, y - 1)
|
||||||
|
local text1b = string.sub(text1, x)
|
||||||
|
local text2b = string.sub(text2, y)
|
||||||
|
|
||||||
|
-- Compute both diffs serially
|
||||||
|
local diffs = StringDiff.findDiffs(text1a, text2a)
|
||||||
|
local diffsB = StringDiff.findDiffs(text1b, text2b)
|
||||||
|
|
||||||
|
-- Merge diffs
|
||||||
|
table.move(diffsB, 1, #diffsB, #diffs + 1, diffs)
|
||||||
|
return diffs
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiff._reorderAndMerge(diffs: Diffs): Diffs
|
||||||
|
-- Reorder and merge like edit sections and merge equalities
|
||||||
|
-- Any edit section can move as long as it doesn't cross an equality
|
||||||
|
|
||||||
|
-- Add a dummy entry at the end
|
||||||
|
table.insert(diffs, { actionType = StringDiff.ActionTypes.Equal, value = "" })
|
||||||
|
|
||||||
|
local pointer = 1
|
||||||
|
local countDelete, countInsert = 0, 0
|
||||||
|
local textDelete, textInsert = "", ""
|
||||||
|
local commonLength
|
||||||
|
while diffs[pointer] do
|
||||||
|
local actionType = diffs[pointer].actionType
|
||||||
|
if actionType == StringDiff.ActionTypes.Insert then
|
||||||
|
countInsert = countInsert + 1
|
||||||
|
textInsert = textInsert .. diffs[pointer].value
|
||||||
|
pointer = pointer + 1
|
||||||
|
elseif actionType == StringDiff.ActionTypes.Delete then
|
||||||
|
countDelete = countDelete + 1
|
||||||
|
textDelete = textDelete .. diffs[pointer].value
|
||||||
|
pointer = pointer + 1
|
||||||
|
elseif actionType == StringDiff.ActionTypes.Equal then
|
||||||
|
-- Upon reaching an equality, check for prior redundancies
|
||||||
|
if countDelete + countInsert > 1 then
|
||||||
|
if (countDelete > 0) and (countInsert > 0) then
|
||||||
|
-- Factor out any common prefixies
|
||||||
|
commonLength = StringDiff._sharedPrefix(textInsert, textDelete)
|
||||||
|
if commonLength > 0 then
|
||||||
|
local back_pointer = pointer - countDelete - countInsert
|
||||||
|
if
|
||||||
|
(back_pointer > 1) and (diffs[back_pointer - 1].actionType == StringDiff.ActionTypes.Equal)
|
||||||
|
then
|
||||||
|
diffs[back_pointer - 1].value = diffs[back_pointer - 1].value
|
||||||
|
.. string.sub(textInsert, 1, commonLength)
|
||||||
|
else
|
||||||
|
table.insert(diffs, 1, {
|
||||||
|
actionType = StringDiff.ActionTypes.Equal,
|
||||||
|
value = string.sub(textInsert, 1, commonLength),
|
||||||
|
})
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
textInsert = string.sub(textInsert, commonLength + 1)
|
||||||
|
textDelete = string.sub(textDelete, commonLength + 1)
|
||||||
|
end
|
||||||
|
-- Factor out any common suffixies
|
||||||
|
commonLength = StringDiff._sharedSuffix(textInsert, textDelete)
|
||||||
|
if commonLength ~= 0 then
|
||||||
|
diffs[pointer].value = string.sub(textInsert, -commonLength) .. diffs[pointer].value
|
||||||
|
textInsert = string.sub(textInsert, 1, -commonLength - 1)
|
||||||
|
textDelete = string.sub(textDelete, 1, -commonLength - 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Delete the offending records and add the merged ones
|
||||||
|
pointer = pointer - countDelete - countInsert
|
||||||
|
for _ = 1, countDelete + countInsert do
|
||||||
|
table.remove(diffs, pointer)
|
||||||
|
end
|
||||||
|
if #textDelete > 0 then
|
||||||
|
table.insert(diffs, pointer, { actionType = StringDiff.ActionTypes.Delete, value = textDelete })
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
if #textInsert > 0 then
|
||||||
|
table.insert(diffs, pointer, { actionType = StringDiff.ActionTypes.Insert, value = textInsert })
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
pointer = pointer + 1
|
||||||
|
elseif (pointer > 1) and (diffs[pointer - 1].actionType == StringDiff.ActionTypes.Equal) then
|
||||||
|
-- Merge this equality with the previous one
|
||||||
|
diffs[pointer - 1].value = diffs[pointer - 1].value .. diffs[pointer].value
|
||||||
|
table.remove(diffs, pointer)
|
||||||
|
else
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
countInsert, countDelete = 0, 0
|
||||||
|
textDelete, textInsert = "", ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if diffs[#diffs].value == "" then
|
||||||
|
-- Remove the dummy entry at the end
|
||||||
|
diffs[#diffs] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Second pass: look for single edits surrounded on both sides by equalities
|
||||||
|
-- which can be shifted sideways to eliminate an equality
|
||||||
|
-- e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
|
||||||
|
local changes = false
|
||||||
|
pointer = 2
|
||||||
|
-- Intentionally ignore the first and last element (don't need checking)
|
||||||
|
while pointer < #diffs 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 currentDiff = diffs[pointer]
|
||||||
|
local currentText = currentDiff.value
|
||||||
|
local prevText = prevDiff.value
|
||||||
|
local nextText = nextDiff.value
|
||||||
|
if #prevText == 0 then
|
||||||
|
table.remove(diffs, pointer - 1)
|
||||||
|
changes = true
|
||||||
|
elseif string.sub(currentText, -#prevText) == prevText then
|
||||||
|
-- Shift the edit over the previous equality
|
||||||
|
currentDiff.value = prevText .. string.sub(currentText, 1, -#prevText - 1)
|
||||||
|
nextDiff.value = prevText .. nextDiff.value
|
||||||
|
table.remove(diffs, pointer - 1)
|
||||||
|
changes = true
|
||||||
|
elseif string.sub(currentText, 1, #nextText) == nextText then
|
||||||
|
-- Shift the edit over the next equality
|
||||||
|
prevDiff.value = prevText .. nextText
|
||||||
|
currentDiff.value = string.sub(currentText, #nextText + 1) .. nextText
|
||||||
|
table.remove(diffs, pointer + 1)
|
||||||
|
changes = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
pointer = pointer + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If shifts were made, the diffs need reordering and another shift sweep
|
||||||
|
if changes then
|
||||||
|
return StringDiff._reorderAndMerge(diffs)
|
||||||
|
end
|
||||||
|
|
||||||
|
return diffs
|
||||||
|
end
|
||||||
|
|
||||||
|
return StringDiff
|
||||||
202
plugin/src/App/Components/StringDiffVisualizer/init.lua
Normal file
202
plugin/src/App/Components/StringDiffVisualizer/init.lua
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
local TextService = game:GetService("TextService")
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
local Log = require(Packages.Log)
|
||||||
|
local Highlighter = require(Packages.Highlighter)
|
||||||
|
local StringDiff = require(script:FindFirstChild("StringDiff"))
|
||||||
|
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
|
local CodeLabel = require(Plugin.App.Components.CodeLabel)
|
||||||
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
self:updateScriptBackground()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
self:calculateContentSize()
|
||||||
|
self:updateScriptBackground()
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
add = {},
|
||||||
|
remove = {},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:willUnmount()
|
||||||
|
self.themeChangedConnection:Disconnect()
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:updateScriptBackground()
|
||||||
|
local backgroundColor = Highlighter.getTokenColor("background")
|
||||||
|
if backgroundColor ~= self.scriptBackground:getValue() then
|
||||||
|
self.setScriptBackground(backgroundColor)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:didUpdate(previousProps)
|
||||||
|
if previousProps.oldText ~= self.props.oldText or previousProps.newText ~= self.props.newText then
|
||||||
|
self:calculateContentSize()
|
||||||
|
local add, remove = self:calculateDiffLines()
|
||||||
|
self:setState({
|
||||||
|
add = add,
|
||||||
|
remove = remove,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:calculateContentSize()
|
||||||
|
local oldText, newText = self.props.oldText, self.props.newText
|
||||||
|
|
||||||
|
local oldTextBounds = TextService:GetTextSize(oldText, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
||||||
|
local newTextBounds = TextService:GetTextSize(newText, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
||||||
|
|
||||||
|
self.setContentSize(
|
||||||
|
Vector2.new(math.max(oldTextBounds.X, newTextBounds.X), math.max(oldTextBounds.Y, newTextBounds.Y))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:calculateDiffLines()
|
||||||
|
local oldText, newText = self.props.oldText, self.props.newText
|
||||||
|
|
||||||
|
-- Diff the two texts
|
||||||
|
local startClock = os.clock()
|
||||||
|
local diffs = StringDiff.findDiffs(oldText, newText)
|
||||||
|
local stopClock = os.clock()
|
||||||
|
|
||||||
|
Log.trace(
|
||||||
|
"Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections",
|
||||||
|
#oldText,
|
||||||
|
#newText,
|
||||||
|
math.round((stopClock - startClock) * 1000 * 1000),
|
||||||
|
#diffs
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Determine which lines to highlight
|
||||||
|
local add, remove = {}, {}
|
||||||
|
|
||||||
|
local oldLineNum, newLineNum = 1, 1
|
||||||
|
for _, diff in diffs do
|
||||||
|
local actionType, text = diff.actionType, diff.value
|
||||||
|
local lines = select(2, string.gsub(text, "\n", "\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
|
||||||
|
else
|
||||||
|
if string.match(text, "%S") then
|
||||||
|
add[newLineNum] = true
|
||||||
|
end
|
||||||
|
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
|
||||||
|
end
|
||||||
|
oldLineNum += lines
|
||||||
|
else
|
||||||
|
Log.warn("Unknown diff action: {} {}", actionType, text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return add, remove
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:render()
|
||||||
|
local oldText, newText = self.props.oldText, self.props.newText
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
return e(BorderedContainer, {
|
||||||
|
size = self.props.size,
|
||||||
|
position = self.props.position,
|
||||||
|
anchorPoint = self.props.anchorPoint,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}, {
|
||||||
|
Background = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
Position = UDim2.new(0, 0, 0, 0),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundColor3 = self.scriptBackground,
|
||||||
|
ZIndex = -10,
|
||||||
|
}, {
|
||||||
|
UICorner = e("UICorner", {
|
||||||
|
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,
|
||||||
|
}, {
|
||||||
|
Source = e(CodeLabel, {
|
||||||
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
position = UDim2.new(0, 0, 0, 0),
|
||||||
|
text = oldText,
|
||||||
|
lineBackground = theme.Diff.Remove,
|
||||||
|
markedLines = self.state.remove,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
New = e(ScrollingFrame, {
|
||||||
|
position = UDim2.new(0.5, 5, 0, 2),
|
||||||
|
size = UDim2.new(0.5, -7, 1, -4),
|
||||||
|
scrollingDirection = Enum.ScrollingDirection.XY,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
contentSize = self.contentSize,
|
||||||
|
}, {
|
||||||
|
Source = e(CodeLabel, {
|
||||||
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
position = UDim2.new(0, 0, 0, 0),
|
||||||
|
text = newText,
|
||||||
|
lineBackground = theme.Diff.Add,
|
||||||
|
markedLines = self.state.add,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return StringDiffVisualizer
|
||||||
|
|
@ -76,6 +76,12 @@ function StudioPluginGui:didUpdate(lastProps)
|
||||||
if self.props.active ~= lastProps.active then
|
if self.props.active ~= lastProps.active then
|
||||||
-- This is intentionally in didUpdate to make sure the initial active state
|
-- This is intentionally in didUpdate to make sure the initial active state
|
||||||
-- (if the PluginGui is open initially) is preserved.
|
-- (if the PluginGui is open initially) is preserved.
|
||||||
|
|
||||||
|
-- Studio widgets are very unreliable and sometimes need to be flickered
|
||||||
|
-- in order to force them to render correctly
|
||||||
|
-- This happens within a single frame so it doesn't flicker visibly
|
||||||
|
self.pluginGui.Enabled = self.props.active
|
||||||
|
self.pluginGui.Enabled = not self.props.active
|
||||||
self.pluginGui.Enabled = self.props.active
|
self.pluginGui.Enabled = self.props.active
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ local Header = require(Plugin.App.Components.Header)
|
||||||
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
|
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
|
||||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||||
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
||||||
|
local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
|
@ -21,6 +22,12 @@ local ConfirmingPage = Roact.Component:extend("ConfirmingPage")
|
||||||
function ConfirmingPage:init()
|
function ConfirmingPage:init()
|
||||||
self.contentSize, self.setContentSize = Roact.createBinding(0)
|
self.contentSize, self.setContentSize = Roact.createBinding(0)
|
||||||
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
self.containerSize, self.setContainerSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
showingSourceDiff = false,
|
||||||
|
oldSource = "",
|
||||||
|
newSource = "",
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
function ConfirmingPage:render()
|
function ConfirmingPage:render()
|
||||||
|
|
@ -55,6 +62,14 @@ function ConfirmingPage:render()
|
||||||
changeListHeaders = { "Property", "Current", "Incoming" },
|
changeListHeaders = { "Property", "Current", "Incoming" },
|
||||||
patch = self.props.confirmData.patch,
|
patch = self.props.confirmData.patch,
|
||||||
instanceMap = self.props.confirmData.instanceMap,
|
instanceMap = self.props.confirmData.instanceMap,
|
||||||
|
|
||||||
|
showSourceDiff = function(oldSource: string, newSource: string)
|
||||||
|
self:setState({
|
||||||
|
showingSourceDiff = true,
|
||||||
|
oldSource = oldSource,
|
||||||
|
newSource = newSource,
|
||||||
|
})
|
||||||
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Buttons = e("Frame", {
|
Buttons = e("Frame", {
|
||||||
|
|
@ -120,6 +135,43 @@ function ConfirmingPage:render()
|
||||||
PaddingLeft = UDim.new(0, 20),
|
PaddingLeft = UDim.new(0, 20),
|
||||||
PaddingRight = UDim.new(0, 20),
|
PaddingRight = UDim.new(0, 20),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
SourceDiff = e(StudioPluginGui, {
|
||||||
|
id = "Rojo_ConfirmingSourceDiff",
|
||||||
|
title = "Source diff",
|
||||||
|
active = self.state.showingSourceDiff,
|
||||||
|
|
||||||
|
initDockState = Enum.InitialDockState.Float,
|
||||||
|
overridePreviousState = true,
|
||||||
|
floatingSize = Vector2.new(500, 350),
|
||||||
|
minimumSize = Vector2.new(400, 250),
|
||||||
|
|
||||||
|
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
|
||||||
|
onClose = function()
|
||||||
|
self:setState({
|
||||||
|
showingSourceDiff = false,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
TooltipsProvider = e(Tooltip.Provider, nil, {
|
||||||
|
Tooltips = e(Tooltip.Container, nil),
|
||||||
|
Content = e("Frame", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
e(StringDiffVisualizer, {
|
||||||
|
size = UDim2.new(1, -10, 1, -10),
|
||||||
|
position = UDim2.new(0, 5, 0, 5),
|
||||||
|
anchorPoint = Vector2.new(0, 0),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
|
oldText = self.state.oldSource,
|
||||||
|
newText = self.state.newSource,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if self.props.createPopup then
|
if self.props.createPopup then
|
||||||
|
|
@ -132,8 +184,7 @@ function ConfirmingPage:render()
|
||||||
active = true,
|
active = true,
|
||||||
|
|
||||||
initDockState = Enum.InitialDockState.Float,
|
initDockState = Enum.InitialDockState.Float,
|
||||||
initEnabled = true,
|
overridePreviousState = false,
|
||||||
overridePreviousState = true,
|
|
||||||
floatingSize = Vector2.new(500, 350),
|
floatingSize = Vector2.new(500, 350),
|
||||||
minimumSize = Vector2.new(400, 250),
|
minimumSize = Vector2.new(400, 250),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,14 @@ local Theme = require(Plugin.App.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local PatchSet = require(Plugin.PatchSet)
|
local PatchSet = require(Plugin.PatchSet)
|
||||||
|
|
||||||
|
local StudioPluginGui = require(Plugin.App.Components.Studio.StudioPluginGui)
|
||||||
local Header = require(Plugin.App.Components.Header)
|
local Header = require(Plugin.App.Components.Header)
|
||||||
local IconButton = require(Plugin.App.Components.IconButton)
|
local IconButton = require(Plugin.App.Components.IconButton)
|
||||||
local TextButton = require(Plugin.App.Components.TextButton)
|
local TextButton = require(Plugin.App.Components.TextButton)
|
||||||
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
local Tooltip = require(Plugin.App.Components.Tooltip)
|
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||||
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
local PatchVisualizer = require(Plugin.App.Components.PatchVisualizer)
|
||||||
|
local StringDiffVisualizer = require(Plugin.App.Components.StringDiffVisualizer)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
|
@ -39,7 +41,7 @@ function timeSinceText(elapsed: number): string
|
||||||
return ageText
|
return ageText
|
||||||
end
|
end
|
||||||
|
|
||||||
local ChangesDrawer = Roact.Component:extend("ConnectedPage")
|
local ChangesDrawer = Roact.Component:extend("ChangesDrawer")
|
||||||
|
|
||||||
function ChangesDrawer:init()
|
function ChangesDrawer:init()
|
||||||
-- Hold onto the serve session during the lifecycle of this component
|
-- Hold onto the serve session during the lifecycle of this component
|
||||||
|
|
@ -84,6 +86,8 @@ function ChangesDrawer:render()
|
||||||
layoutOrder = 3,
|
layoutOrder = 3,
|
||||||
|
|
||||||
patchTree = self.props.patchTree,
|
patchTree = self.props.patchTree,
|
||||||
|
|
||||||
|
showSourceDiff = self.props.showSourceDiff,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|
@ -226,6 +230,9 @@ function ConnectedPage:init()
|
||||||
self:setState({
|
self:setState({
|
||||||
renderChanges = false,
|
renderChanges = false,
|
||||||
hoveringChangeInfo = false,
|
hoveringChangeInfo = false,
|
||||||
|
showingSourceDiff = false,
|
||||||
|
oldSource = "",
|
||||||
|
newSource = "",
|
||||||
})
|
})
|
||||||
|
|
||||||
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
|
self.changeInfoText, self.setChangeInfoText = Roact.createBinding("")
|
||||||
|
|
@ -239,7 +246,11 @@ end
|
||||||
|
|
||||||
function ConnectedPage:didUpdate(previousProps)
|
function ConnectedPage:didUpdate(previousProps)
|
||||||
if self.props.patchData.timestamp ~= previousProps.patchData.timestamp then
|
if self.props.patchData.timestamp ~= previousProps.patchData.timestamp then
|
||||||
|
-- New patch recieved
|
||||||
self:startChangeInfoTextUpdater()
|
self:startChangeInfoTextUpdater()
|
||||||
|
self:setState({
|
||||||
|
showingSourceDiff = false,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -367,6 +378,14 @@ function ConnectedPage:render()
|
||||||
height = self.changeDrawerHeight,
|
height = self.changeDrawerHeight,
|
||||||
layoutOrder = 5,
|
layoutOrder = 5,
|
||||||
|
|
||||||
|
showSourceDiff = function(oldSource: string, newSource: string)
|
||||||
|
self:setState({
|
||||||
|
showingSourceDiff = true,
|
||||||
|
oldSource = oldSource,
|
||||||
|
newSource = newSource,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
|
||||||
onClose = function()
|
onClose = function()
|
||||||
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
self.changeDrawerMotor:setGoal(Flipper.Spring.new(0, {
|
||||||
frequency = 4,
|
frequency = 4,
|
||||||
|
|
@ -374,6 +393,43 @@ function ConnectedPage:render()
|
||||||
}))
|
}))
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
SourceDiff = e(StudioPluginGui, {
|
||||||
|
id = "Rojo_ConnectedSourceDiff",
|
||||||
|
title = "Source diff",
|
||||||
|
active = self.state.showingSourceDiff,
|
||||||
|
|
||||||
|
initDockState = Enum.InitialDockState.Float,
|
||||||
|
overridePreviousState = false,
|
||||||
|
floatingSize = Vector2.new(500, 350),
|
||||||
|
minimumSize = Vector2.new(400, 250),
|
||||||
|
|
||||||
|
zIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
||||||
|
|
||||||
|
onClose = function()
|
||||||
|
self:setState({
|
||||||
|
showingSourceDiff = false,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
TooltipsProvider = e(Tooltip.Provider, nil, {
|
||||||
|
Tooltips = e(Tooltip.Container, nil),
|
||||||
|
Content = e("Frame", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
e(StringDiffVisualizer, {
|
||||||
|
size = UDim2.new(1, -10, 1, -10),
|
||||||
|
position = UDim2.new(0, 5, 0, 5),
|
||||||
|
anchorPoint = Vector2.new(0, 0),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
|
oldText = self.state.oldSource,
|
||||||
|
newText = self.state.newSource,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -23,207 +23,198 @@ local Log = require(Packages.Log)
|
||||||
|
|
||||||
local strict = require(script.Parent.Parent.strict)
|
local strict = require(script.Parent.Parent.strict)
|
||||||
|
|
||||||
-- Copying hex colors back and forth from design programs is faster
|
local BRAND_COLOR = Color3.fromHex("E13835")
|
||||||
local function hexColor(decimal)
|
|
||||||
local red = bit32.band(bit32.rshift(decimal, 16), 2^8 - 1)
|
|
||||||
local green = bit32.band(bit32.rshift(decimal, 8), 2^8 - 1)
|
|
||||||
local blue = bit32.band(decimal, 2^8 - 1)
|
|
||||||
|
|
||||||
return Color3.fromRGB(red, green, blue)
|
|
||||||
end
|
|
||||||
|
|
||||||
local BRAND_COLOR = hexColor(0xE13835)
|
|
||||||
|
|
||||||
local lightTheme = strict("LightTheme", {
|
local lightTheme = strict("LightTheme", {
|
||||||
BackgroundColor = hexColor(0xFFFFFF),
|
BackgroundColor = Color3.fromHex("FFFFFF"),
|
||||||
Button = {
|
Button = {
|
||||||
Solid = {
|
Solid = {
|
||||||
ActionFillColor = hexColor(0xFFFFFF),
|
ActionFillColor = Color3.fromHex("FFFFFF"),
|
||||||
ActionFillTransparency = 0.8,
|
ActionFillTransparency = 0.8,
|
||||||
Enabled = {
|
Enabled = {
|
||||||
TextColor = hexColor(0xFFFFFF),
|
TextColor = Color3.fromHex("FFFFFF"),
|
||||||
BackgroundColor = BRAND_COLOR,
|
BackgroundColor = BRAND_COLOR,
|
||||||
},
|
},
|
||||||
Disabled = {
|
Disabled = {
|
||||||
TextColor = hexColor(0xFFFFFF),
|
TextColor = Color3.fromHex("FFFFFF"),
|
||||||
BackgroundColor = BRAND_COLOR,
|
BackgroundColor = BRAND_COLOR,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Bordered = {
|
Bordered = {
|
||||||
ActionFillColor = hexColor(0x000000),
|
ActionFillColor = Color3.fromHex("000000"),
|
||||||
ActionFillTransparency = 0.9,
|
ActionFillTransparency = 0.9,
|
||||||
Enabled = {
|
Enabled = {
|
||||||
TextColor = hexColor(0x393939),
|
TextColor = Color3.fromHex("393939"),
|
||||||
BorderColor = hexColor(0xACACAC),
|
BorderColor = Color3.fromHex("ACACAC"),
|
||||||
},
|
},
|
||||||
Disabled = {
|
Disabled = {
|
||||||
TextColor = hexColor(0x393939),
|
TextColor = Color3.fromHex("393939"),
|
||||||
BorderColor = hexColor(0xACACAC),
|
BorderColor = Color3.fromHex("ACACAC"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Checkbox = {
|
Checkbox = {
|
||||||
Active = {
|
Active = {
|
||||||
IconColor = hexColor(0xFFFFFF),
|
IconColor = Color3.fromHex("FFFFFF"),
|
||||||
BackgroundColor = BRAND_COLOR,
|
BackgroundColor = BRAND_COLOR,
|
||||||
},
|
},
|
||||||
Inactive = {
|
Inactive = {
|
||||||
IconColor = hexColor(0xEEEEEE),
|
IconColor = Color3.fromHex("EEEEEE"),
|
||||||
BorderColor = hexColor(0xAFAFAF),
|
BorderColor = Color3.fromHex("AFAFAF"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Dropdown = {
|
Dropdown = {
|
||||||
TextColor = hexColor(0x00000),
|
TextColor = Color3.fromHex("000000"),
|
||||||
BorderColor = hexColor(0xAFAFAF),
|
BorderColor = Color3.fromHex("AFAFAF"),
|
||||||
BackgroundColor = hexColor(0xEEEEEE),
|
BackgroundColor = Color3.fromHex("EEEEEE"),
|
||||||
Open = {
|
Open = {
|
||||||
IconColor = BRAND_COLOR,
|
IconColor = BRAND_COLOR,
|
||||||
},
|
},
|
||||||
Closed = {
|
Closed = {
|
||||||
IconColor = hexColor(0xEEEEEE),
|
IconColor = Color3.fromHex("EEEEEE"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AddressEntry = {
|
AddressEntry = {
|
||||||
TextColor = hexColor(0x000000),
|
TextColor = Color3.fromHex("000000"),
|
||||||
PlaceholderColor = hexColor(0x8C8C8C)
|
PlaceholderColor = Color3.fromHex("8C8C8C")
|
||||||
},
|
},
|
||||||
BorderedContainer = {
|
BorderedContainer = {
|
||||||
BorderColor = hexColor(0xCBCBCB),
|
BorderColor = Color3.fromHex("CBCBCB"),
|
||||||
BackgroundColor = hexColor(0xEEEEEE),
|
BackgroundColor = Color3.fromHex("EEEEEE"),
|
||||||
},
|
},
|
||||||
Spinner = {
|
Spinner = {
|
||||||
ForegroundColor = BRAND_COLOR,
|
ForegroundColor = BRAND_COLOR,
|
||||||
BackgroundColor = hexColor(0xEEEEEE),
|
BackgroundColor = Color3.fromHex("EEEEEE"),
|
||||||
},
|
},
|
||||||
Diff = {
|
Diff = {
|
||||||
Add = hexColor(0xbaffbd),
|
Add = Color3.fromHex("baffbd"),
|
||||||
Remove = hexColor(0xffbdba),
|
Remove = Color3.fromHex("ffbdba"),
|
||||||
Edit = hexColor(0xbacdff),
|
Edit = Color3.fromHex("bacdff"),
|
||||||
Row = hexColor(0x000000),
|
Row = Color3.fromHex("000000"),
|
||||||
Warning = hexColor(0xFF8E3C),
|
Warning = Color3.fromHex("FF8E3C"),
|
||||||
},
|
},
|
||||||
ConnectionDetails = {
|
ConnectionDetails = {
|
||||||
ProjectNameColor = hexColor(0x00000),
|
ProjectNameColor = Color3.fromHex("000000"),
|
||||||
AddressColor = hexColor(0x00000),
|
AddressColor = Color3.fromHex("000000"),
|
||||||
DisconnectColor = BRAND_COLOR,
|
DisconnectColor = BRAND_COLOR,
|
||||||
},
|
},
|
||||||
Settings = {
|
Settings = {
|
||||||
DividerColor = hexColor(0xCBCBCB),
|
DividerColor = Color3.fromHex("CBCBCB"),
|
||||||
Navbar = {
|
Navbar = {
|
||||||
BackButtonColor = hexColor(0x000000),
|
BackButtonColor = Color3.fromHex("000000"),
|
||||||
TextColor = hexColor(0x000000),
|
TextColor = Color3.fromHex("000000"),
|
||||||
},
|
},
|
||||||
Setting = {
|
Setting = {
|
||||||
NameColor = hexColor(0x000000),
|
NameColor = Color3.fromHex("000000"),
|
||||||
DescriptionColor = hexColor(0x5F5F5F),
|
DescriptionColor = Color3.fromHex("5F5F5F"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Header = {
|
Header = {
|
||||||
LogoColor = BRAND_COLOR,
|
LogoColor = BRAND_COLOR,
|
||||||
VersionColor = hexColor(0x727272),
|
VersionColor = Color3.fromHex("727272"),
|
||||||
},
|
},
|
||||||
Notification = {
|
Notification = {
|
||||||
InfoColor = hexColor(0x00000),
|
InfoColor = Color3.fromHex("000000"),
|
||||||
CloseColor = BRAND_COLOR,
|
CloseColor = BRAND_COLOR,
|
||||||
},
|
},
|
||||||
ErrorColor = hexColor(0x000000),
|
ErrorColor = Color3.fromHex("000000"),
|
||||||
ScrollBarColor = hexColor(0x000000),
|
ScrollBarColor = Color3.fromHex("000000"),
|
||||||
})
|
})
|
||||||
|
|
||||||
local darkTheme = strict("DarkTheme", {
|
local darkTheme = strict("DarkTheme", {
|
||||||
BackgroundColor = hexColor(0x2E2E2E),
|
BackgroundColor = Color3.fromHex("2E2E2E"),
|
||||||
Button = {
|
Button = {
|
||||||
Solid = {
|
Solid = {
|
||||||
ActionFillColor = hexColor(0xFFFFFF),
|
ActionFillColor = Color3.fromHex("FFFFFF"),
|
||||||
ActionFillTransparency = 0.8,
|
ActionFillTransparency = 0.8,
|
||||||
Enabled = {
|
Enabled = {
|
||||||
TextColor = hexColor(0xFFFFFF),
|
TextColor = Color3.fromHex("FFFFFF"),
|
||||||
BackgroundColor = BRAND_COLOR,
|
BackgroundColor = BRAND_COLOR,
|
||||||
},
|
},
|
||||||
Disabled = {
|
Disabled = {
|
||||||
TextColor = hexColor(0xFFFFFF),
|
TextColor = Color3.fromHex("FFFFFF"),
|
||||||
BackgroundColor = BRAND_COLOR,
|
BackgroundColor = BRAND_COLOR,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Bordered = {
|
Bordered = {
|
||||||
ActionFillColor = hexColor(0xFFFFFF),
|
ActionFillColor = Color3.fromHex("FFFFFF"),
|
||||||
ActionFillTransparency = 0.9,
|
ActionFillTransparency = 0.9,
|
||||||
Enabled = {
|
Enabled = {
|
||||||
TextColor = hexColor(0xDBDBDB),
|
TextColor = Color3.fromHex("DBDBDB"),
|
||||||
BorderColor = hexColor(0x535353),
|
BorderColor = Color3.fromHex("535353"),
|
||||||
},
|
},
|
||||||
Disabled = {
|
Disabled = {
|
||||||
TextColor = hexColor(0xDBDBDB),
|
TextColor = Color3.fromHex("DBDBDB"),
|
||||||
BorderColor = hexColor(0x535353),
|
BorderColor = Color3.fromHex("535353"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Checkbox = {
|
Checkbox = {
|
||||||
Active = {
|
Active = {
|
||||||
IconColor = hexColor(0xFFFFFF),
|
IconColor = Color3.fromHex("FFFFFF"),
|
||||||
BackgroundColor = BRAND_COLOR,
|
BackgroundColor = BRAND_COLOR,
|
||||||
},
|
},
|
||||||
Inactive = {
|
Inactive = {
|
||||||
IconColor = hexColor(0x484848),
|
IconColor = Color3.fromHex("484848"),
|
||||||
BorderColor = hexColor(0x5A5A5A),
|
BorderColor = Color3.fromHex("5A5A5A"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Dropdown = {
|
Dropdown = {
|
||||||
TextColor = hexColor(0xFFFFFF),
|
TextColor = Color3.fromHex("FFFFFF"),
|
||||||
BorderColor = hexColor(0x5A5A5A),
|
BorderColor = Color3.fromHex("5A5A5A"),
|
||||||
BackgroundColor = hexColor(0x2B2B2B),
|
BackgroundColor = Color3.fromHex("2B2B2B"),
|
||||||
Open = {
|
Open = {
|
||||||
IconColor = BRAND_COLOR,
|
IconColor = BRAND_COLOR,
|
||||||
},
|
},
|
||||||
Closed = {
|
Closed = {
|
||||||
IconColor = hexColor(0x484848),
|
IconColor = Color3.fromHex("484848"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AddressEntry = {
|
AddressEntry = {
|
||||||
TextColor = hexColor(0xFFFFFF),
|
TextColor = Color3.fromHex("FFFFFF"),
|
||||||
PlaceholderColor = hexColor(0x8B8B8B)
|
PlaceholderColor = Color3.fromHex("8B8B8B")
|
||||||
},
|
},
|
||||||
BorderedContainer = {
|
BorderedContainer = {
|
||||||
BorderColor = hexColor(0x535353),
|
BorderColor = Color3.fromHex("535353"),
|
||||||
BackgroundColor = hexColor(0x2B2B2B),
|
BackgroundColor = Color3.fromHex("2B2B2B"),
|
||||||
},
|
},
|
||||||
Spinner = {
|
Spinner = {
|
||||||
ForegroundColor = BRAND_COLOR,
|
ForegroundColor = BRAND_COLOR,
|
||||||
BackgroundColor = hexColor(0x2B2B2B),
|
BackgroundColor = Color3.fromHex("2B2B2B"),
|
||||||
},
|
},
|
||||||
Diff = {
|
Diff = {
|
||||||
Add = hexColor(0x273732),
|
Add = Color3.fromHex("273732"),
|
||||||
Remove = hexColor(0x3F2D32),
|
Remove = Color3.fromHex("3F2D32"),
|
||||||
Edit = hexColor(0x193345),
|
Edit = Color3.fromHex("193345"),
|
||||||
Row = hexColor(0xFFFFFF),
|
Row = Color3.fromHex("FFFFFF"),
|
||||||
Warning = hexColor(0xFF8E3C),
|
Warning = Color3.fromHex("FF8E3C"),
|
||||||
},
|
},
|
||||||
ConnectionDetails = {
|
ConnectionDetails = {
|
||||||
ProjectNameColor = hexColor(0xFFFFFF),
|
ProjectNameColor = Color3.fromHex("FFFFFF"),
|
||||||
AddressColor = hexColor(0xFFFFFF),
|
AddressColor = Color3.fromHex("FFFFFF"),
|
||||||
DisconnectColor = hexColor(0xFFFFFF),
|
DisconnectColor = Color3.fromHex("FFFFFF"),
|
||||||
},
|
},
|
||||||
Settings = {
|
Settings = {
|
||||||
DividerColor = hexColor(0x535353),
|
DividerColor = Color3.fromHex("535353"),
|
||||||
Navbar = {
|
Navbar = {
|
||||||
BackButtonColor = hexColor(0xFFFFFF),
|
BackButtonColor = Color3.fromHex("FFFFFF"),
|
||||||
TextColor = hexColor(0xFFFFFF),
|
TextColor = Color3.fromHex("FFFFFF"),
|
||||||
},
|
},
|
||||||
Setting = {
|
Setting = {
|
||||||
NameColor = hexColor(0xFFFFFF),
|
NameColor = Color3.fromHex("FFFFFF"),
|
||||||
DescriptionColor = hexColor(0xD3D3D3),
|
DescriptionColor = Color3.fromHex("D3D3D3"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Header = {
|
Header = {
|
||||||
LogoColor = BRAND_COLOR,
|
LogoColor = BRAND_COLOR,
|
||||||
VersionColor = hexColor(0xD3D3D3)
|
VersionColor = Color3.fromHex("D3D3D3")
|
||||||
},
|
},
|
||||||
Notification = {
|
Notification = {
|
||||||
InfoColor = hexColor(0xFFFFFF),
|
InfoColor = Color3.fromHex("FFFFFF"),
|
||||||
CloseColor = hexColor(0xFFFFFF),
|
CloseColor = Color3.fromHex("FFFFFF"),
|
||||||
},
|
},
|
||||||
ErrorColor = hexColor(0xFFFFFF),
|
ErrorColor = Color3.fromHex("FFFFFF"),
|
||||||
ScrollBarColor = hexColor(0xFFFFFF),
|
ScrollBarColor = Color3.fromHex("FFFFFF"),
|
||||||
})
|
})
|
||||||
|
|
||||||
local Context = Roact.createContext(lightTheme)
|
local Context = Roact.createContext(lightTheme)
|
||||||
|
|
|
||||||
|
|
@ -717,7 +717,6 @@ function App:render()
|
||||||
active = self.state.guiEnabled,
|
active = self.state.guiEnabled,
|
||||||
|
|
||||||
initDockState = Enum.InitialDockState.Right,
|
initDockState = Enum.InitialDockState.Right,
|
||||||
initEnabled = false,
|
|
||||||
overridePreviousState = false,
|
overridePreviousState = false,
|
||||||
floatingSize = Vector2.new(320, 210),
|
floatingSize = Vector2.new(320, 210),
|
||||||
minimumSize = Vector2.new(300, 210),
|
minimumSize = Vector2.new(300, 210),
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,10 @@ function Tree:getNode(id, searchNode)
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Tree:doesNodeExist(id)
|
||||||
|
return self.idToNode[id] ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
-- Adds a node to the tree as a child of the node with id == parent
|
-- Adds a node to the tree as a child of the node with id == parent
|
||||||
-- If parent is nil, it defaults to root
|
-- If parent is nil, it defaults to root
|
||||||
-- props must contain id, and cannot contain children or parentId
|
-- props must contain id, and cannot contain children or parentId
|
||||||
|
|
@ -122,16 +126,16 @@ function Tree:addNode(parent, props)
|
||||||
|
|
||||||
parent = parent or "ROOT"
|
parent = parent or "ROOT"
|
||||||
|
|
||||||
local node = self:getNode(props.id)
|
if self:doesNodeExist(props.id) then
|
||||||
if node then
|
|
||||||
-- Update existing node
|
-- Update existing node
|
||||||
|
local node = self:getNode(props.id)
|
||||||
for k, v in props do
|
for k, v in props do
|
||||||
node[k] = v
|
node[k] = v
|
||||||
end
|
end
|
||||||
return node
|
return node
|
||||||
end
|
end
|
||||||
|
|
||||||
node = table.clone(props)
|
local node = table.clone(props)
|
||||||
node.children = {}
|
node.children = {}
|
||||||
node.parentId = parent
|
node.parentId = parent
|
||||||
|
|
||||||
|
|
@ -149,9 +153,10 @@ end
|
||||||
|
|
||||||
-- Given a list of ancestor ids in descending order, builds the nodes for them
|
-- Given a list of ancestor ids in descending order, builds the nodes for them
|
||||||
-- using the patch and instanceMap info
|
-- using the patch and instanceMap info
|
||||||
function Tree:buildAncestryNodes(ancestryIds: { string }, patch, instanceMap)
|
function Tree:buildAncestryNodes(previousId: string?, ancestryIds: { string }, patch, instanceMap)
|
||||||
-- Build nodes for ancestry by going up the tree
|
-- Build nodes for ancestry by going up the tree
|
||||||
local previousId = "ROOT"
|
previousId = previousId or "ROOT"
|
||||||
|
|
||||||
for _, ancestorId in ancestryIds do
|
for _, ancestorId in ancestryIds do
|
||||||
local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
|
local value = instanceMap.fromIds[ancestorId] or patch.added[ancestorId]
|
||||||
if not value then
|
if not value then
|
||||||
|
|
@ -175,6 +180,8 @@ local PatchTree = {}
|
||||||
function PatchTree.build(patch, instanceMap, changeListHeaders)
|
function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||||
local tree = Tree.new()
|
local tree = Tree.new()
|
||||||
|
|
||||||
|
local knownAncestors = {}
|
||||||
|
|
||||||
for _, change in patch.updated do
|
for _, change in patch.updated do
|
||||||
local instance = instanceMap.fromIds[change.id]
|
local instance = instanceMap.fromIds[change.id]
|
||||||
if not instance then
|
if not instance then
|
||||||
|
|
@ -185,13 +192,21 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||||
local ancestryIds = {}
|
local ancestryIds = {}
|
||||||
local parentObject = instance.Parent
|
local parentObject = instance.Parent
|
||||||
local parentId = instanceMap.fromInstances[parentObject]
|
local parentId = instanceMap.fromInstances[parentObject]
|
||||||
|
local previousId = nil
|
||||||
while parentObject do
|
while parentObject do
|
||||||
|
if knownAncestors[parentId] then
|
||||||
|
-- We've already added this ancestor
|
||||||
|
previousId = parentId
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
table.insert(ancestryIds, 1, parentId)
|
table.insert(ancestryIds, 1, parentId)
|
||||||
|
knownAncestors[parentId] = true
|
||||||
parentObject = parentObject.Parent
|
parentObject = parentObject.Parent
|
||||||
parentId = instanceMap.fromInstances[parentObject]
|
parentId = instanceMap.fromInstances[parentObject]
|
||||||
end
|
end
|
||||||
|
|
||||||
tree:buildAncestryNodes(ancestryIds, patch, instanceMap)
|
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
|
||||||
|
|
||||||
-- Gather detail text
|
-- Gather detail text
|
||||||
local changeList, hint = nil, nil
|
local changeList, hint = nil, nil
|
||||||
|
|
@ -268,14 +283,22 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||||
local ancestryIds = {}
|
local ancestryIds = {}
|
||||||
local parentObject = instance.Parent
|
local parentObject = instance.Parent
|
||||||
local parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
|
local parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
|
||||||
|
local previousId = nil
|
||||||
while parentObject do
|
while parentObject do
|
||||||
|
if knownAncestors[parentId] then
|
||||||
|
-- We've already added this ancestor
|
||||||
|
previousId = parentId
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
instanceMap:insert(parentId, parentObject) -- This ensures we can find the parent later
|
instanceMap:insert(parentId, parentObject) -- This ensures we can find the parent later
|
||||||
table.insert(ancestryIds, 1, parentId)
|
table.insert(ancestryIds, 1, parentId)
|
||||||
|
knownAncestors[parentId] = true
|
||||||
parentObject = parentObject.Parent
|
parentObject = parentObject.Parent
|
||||||
parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
|
parentId = instanceMap.fromInstances[parentObject] or HttpService:GenerateGUID(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
tree:buildAncestryNodes(ancestryIds, patch, instanceMap)
|
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
|
||||||
|
|
||||||
-- Add this node to tree
|
-- Add this node to tree
|
||||||
local nodeId = instanceMap.fromInstances[instance] or HttpService:GenerateGUID(false)
|
local nodeId = instanceMap.fromInstances[instance] or HttpService:GenerateGUID(false)
|
||||||
|
|
@ -295,8 +318,16 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||||
local parentId = change.Parent
|
local parentId = change.Parent
|
||||||
local parentData = patch.added[parentId]
|
local parentData = patch.added[parentId]
|
||||||
local parentObject = instanceMap.fromIds[parentId]
|
local parentObject = instanceMap.fromIds[parentId]
|
||||||
|
local previousId = nil
|
||||||
while parentId do
|
while parentId do
|
||||||
|
if knownAncestors[parentId] then
|
||||||
|
-- We've already added this ancestor
|
||||||
|
previousId = parentId
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
table.insert(ancestryIds, 1, parentId)
|
table.insert(ancestryIds, 1, parentId)
|
||||||
|
knownAncestors[parentId] = true
|
||||||
parentId = nil
|
parentId = nil
|
||||||
|
|
||||||
if parentData then
|
if parentData then
|
||||||
|
|
@ -312,7 +343,7 @@ function PatchTree.build(patch, instanceMap, changeListHeaders)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
tree:buildAncestryNodes(ancestryIds, patch, instanceMap)
|
tree:buildAncestryNodes(previousId, ancestryIds, patch, instanceMap)
|
||||||
|
|
||||||
-- Gather detail text
|
-- Gather detail text
|
||||||
local changeList, hint = nil, nil
|
local changeList, hint = nil, nil
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,11 @@ static PLACE_PROJECT: &str =
|
||||||
static PLACE_README: &str = include_str!("../../assets/default-place-project/README.md");
|
static PLACE_README: &str = include_str!("../../assets/default-place-project/README.md");
|
||||||
static PLACE_GIT_IGNORE: &str = include_str!("../../assets/default-place-project/gitignore.txt");
|
static PLACE_GIT_IGNORE: &str = include_str!("../../assets/default-place-project/gitignore.txt");
|
||||||
|
|
||||||
|
static PLUGIN_PROJECT: &str =
|
||||||
|
include_str!("../../assets/default-plugin-project/default.project.json");
|
||||||
|
static PLUGIN_README: &str = include_str!("../../assets/default-plugin-project/README.md");
|
||||||
|
static PLUGIN_GIT_IGNORE: &str = include_str!("../../assets/default-plugin-project/gitignore.txt");
|
||||||
|
|
||||||
/// Initializes a new Rojo project.
|
/// Initializes a new Rojo project.
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
pub struct InitCommand {
|
pub struct InitCommand {
|
||||||
|
|
@ -28,7 +33,7 @@ pub struct InitCommand {
|
||||||
#[clap(default_value = "")]
|
#[clap(default_value = "")]
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
|
||||||
/// The kind of project to create, 'place' or 'model'. Defaults to place.
|
/// The kind of project to create, 'place', 'plugin', or 'model'. Defaults to place.
|
||||||
#[clap(long, default_value = "place")]
|
#[clap(long, default_value = "place")]
|
||||||
pub kind: InitKind,
|
pub kind: InitKind,
|
||||||
}
|
}
|
||||||
|
|
@ -51,6 +56,7 @@ impl InitCommand {
|
||||||
match self.kind {
|
match self.kind {
|
||||||
InitKind::Place => init_place(&base_path, project_params)?,
|
InitKind::Place => init_place(&base_path, project_params)?,
|
||||||
InitKind::Model => init_model(&base_path, project_params)?,
|
InitKind::Model => init_model(&base_path, project_params)?,
|
||||||
|
InitKind::Plugin => init_plugin(&base_path, project_params)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Created project successfully.");
|
println!("Created project successfully.");
|
||||||
|
|
@ -65,8 +71,11 @@ pub enum InitKind {
|
||||||
/// A place that contains a baseplate.
|
/// A place that contains a baseplate.
|
||||||
Place,
|
Place,
|
||||||
|
|
||||||
/// An empty model, suitable for a library or plugin.
|
/// An empty model, suitable for a library.
|
||||||
Model,
|
Model,
|
||||||
|
|
||||||
|
/// An empty plugin.
|
||||||
|
Plugin,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for InitKind {
|
impl FromStr for InitKind {
|
||||||
|
|
@ -76,8 +85,9 @@ impl FromStr for InitKind {
|
||||||
match source {
|
match source {
|
||||||
"place" => Ok(InitKind::Place),
|
"place" => Ok(InitKind::Place),
|
||||||
"model" => Ok(InitKind::Model),
|
"model" => Ok(InitKind::Model),
|
||||||
|
"plugin" => Ok(InitKind::Plugin),
|
||||||
_ => Err(format_err!(
|
_ => Err(format_err!(
|
||||||
"Invalid init kind '{}'. Valid kinds are: place, model",
|
"Invalid init kind '{}'. Valid kinds are: place, model, plugin",
|
||||||
source
|
source
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
|
|
@ -147,6 +157,29 @@ fn init_model(base_path: &Path, project_params: ProjectParams) -> anyhow::Result
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_plugin(base_path: &Path, project_params: ProjectParams) -> anyhow::Result<()> {
|
||||||
|
println!("Creating new plugin project '{}'", project_params.name);
|
||||||
|
|
||||||
|
let project_file = project_params.render_template(PLUGIN_PROJECT);
|
||||||
|
try_create_project(base_path, &project_file)?;
|
||||||
|
|
||||||
|
let readme = project_params.render_template(PLUGIN_README);
|
||||||
|
write_if_not_exists(&base_path.join("README.md"), &readme)?;
|
||||||
|
|
||||||
|
let src = base_path.join("src");
|
||||||
|
fs::create_dir_all(&src)?;
|
||||||
|
|
||||||
|
write_if_not_exists(
|
||||||
|
&src.join("init.server.lua"),
|
||||||
|
"print(\"Hello world, from plugin!\")\n",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let git_ignore = project_params.render_template(PLUGIN_GIT_IGNORE);
|
||||||
|
try_git_init(base_path, &git_ignore)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Contains parameters used in templates to create a project.
|
/// Contains parameters used in templates to create a project.
|
||||||
struct ProjectParams {
|
struct ProjectParams {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ use std::borrow::Borrow;
|
||||||
|
|
||||||
use anyhow::{bail, format_err};
|
use anyhow::{bail, format_err};
|
||||||
use rbx_dom_weak::types::{
|
use rbx_dom_weak::types::{
|
||||||
Attributes, CFrame, Color3, Content, Enum, Font, Matrix3, Tags, Variant, VariantType, Vector2,
|
Attributes, CFrame, Color3, Content, Enum, Font, MaterialColors, Matrix3, Tags, Variant,
|
||||||
Vector3,
|
VariantType, Vector2, Vector3,
|
||||||
};
|
};
|
||||||
use rbx_reflection::{DataType, PropertyDescriptor};
|
use rbx_reflection::{DataType, PropertyDescriptor};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -50,6 +50,7 @@ pub enum AmbiguousValue {
|
||||||
Array12([f64; 12]),
|
Array12([f64; 12]),
|
||||||
Attributes(Attributes),
|
Attributes(Attributes),
|
||||||
Font(Font),
|
Font(Font),
|
||||||
|
MaterialColors(MaterialColors),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AmbiguousValue {
|
impl AmbiguousValue {
|
||||||
|
|
@ -142,6 +143,10 @@ impl AmbiguousValue {
|
||||||
|
|
||||||
(VariantType::Font, AmbiguousValue::Font(value)) => Ok(value.into()),
|
(VariantType::Font, AmbiguousValue::Font(value)) => Ok(value.into()),
|
||||||
|
|
||||||
|
(VariantType::MaterialColors, AmbiguousValue::MaterialColors(value)) => {
|
||||||
|
Ok(value.into())
|
||||||
|
}
|
||||||
|
|
||||||
(_, unresolved) => Err(format_err!(
|
(_, unresolved) => Err(format_err!(
|
||||||
"Wrong type of value for property {}.{}. Expected {:?}, got {}",
|
"Wrong type of value for property {}.{}. Expected {:?}, got {}",
|
||||||
class_name,
|
class_name,
|
||||||
|
|
@ -180,6 +185,7 @@ impl AmbiguousValue {
|
||||||
AmbiguousValue::Array12(_) => "an array of twelve numbers",
|
AmbiguousValue::Array12(_) => "an array of twelve numbers",
|
||||||
AmbiguousValue::Attributes(_) => "an object containing attributes",
|
AmbiguousValue::Attributes(_) => "an object containing attributes",
|
||||||
AmbiguousValue::Font(_) => "an object describing a Font",
|
AmbiguousValue::Font(_) => "an object describing a Font",
|
||||||
|
AmbiguousValue::MaterialColors(_) => "an object describing MaterialColors",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -351,4 +357,27 @@ mod test {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn material_colors() {
|
||||||
|
use rbx_dom_weak::types::{Color3uint8, TerrainMaterials};
|
||||||
|
|
||||||
|
let mut material_colors = MaterialColors::new();
|
||||||
|
material_colors.set_color(TerrainMaterials::Grass, Color3uint8::new(10, 20, 30));
|
||||||
|
material_colors.set_color(TerrainMaterials::Asphalt, Color3uint8::new(40, 50, 60));
|
||||||
|
material_colors.set_color(TerrainMaterials::LeafyGrass, Color3uint8::new(255, 155, 55));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolve(
|
||||||
|
"Terrain",
|
||||||
|
"MaterialColors",
|
||||||
|
r#"{
|
||||||
|
"Grass": [10, 20, 30],
|
||||||
|
"Asphalt": [40, 50, 60],
|
||||||
|
"LeafyGrass": [255, 155, 55]
|
||||||
|
}"#
|
||||||
|
),
|
||||||
|
Variant::MaterialColors(material_colors)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -301,6 +301,11 @@ fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option<Cow<'stati
|
||||||
if name == "StarterPlayerScripts" || name == "StarterCharacterScripts" {
|
if name == "StarterPlayerScripts" || name == "StarterCharacterScripts" {
|
||||||
return Some(Cow::Owned(name.to_owned()));
|
return Some(Cow::Owned(name.to_owned()));
|
||||||
}
|
}
|
||||||
|
} else if parent_class == "Workspace" {
|
||||||
|
// Workspace has a special Terrain class inside it
|
||||||
|
if name == "Terrain" {
|
||||||
|
return Some(Cow::Owned(name.to_owned()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
use std::{collections::HashMap, fs, path::PathBuf, str::FromStr, sync::Arc};
|
use std::{collections::HashMap, fs, path::PathBuf, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
use hyper::{body, Body, Method, Request, Response, StatusCode};
|
use hyper::{body, Body, Method, Request, Response, StatusCode};
|
||||||
|
use opener::OpenError;
|
||||||
use rbx_dom_weak::types::Ref;
|
use rbx_dom_weak::types::Ref;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -236,7 +237,39 @@ impl ApiService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = opener::open(script_path);
|
match opener::open(&script_path) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(error) => match error {
|
||||||
|
OpenError::Io(io_error) => {
|
||||||
|
return json(
|
||||||
|
ErrorResponse::internal_error(format!(
|
||||||
|
"Attempting to open {} failed because of the following io error: {}",
|
||||||
|
script_path.display(),
|
||||||
|
io_error
|
||||||
|
)),
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OpenError::ExitStatus {
|
||||||
|
cmd,
|
||||||
|
status,
|
||||||
|
stderr,
|
||||||
|
} => {
|
||||||
|
return json(
|
||||||
|
ErrorResponse::internal_error(format!(
|
||||||
|
r#"The command '{}' to open '{}' failed with the error code '{}'.
|
||||||
|
Error logs:
|
||||||
|
{}"#,
|
||||||
|
cmd,
|
||||||
|
script_path.display(),
|
||||||
|
status,
|
||||||
|
stderr
|
||||||
|
)),
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
json_ok(&OpenResponse {
|
json_ok(&OpenResponse {
|
||||||
session_id: self.serve_session.session_id(),
|
session_id: self.serve_session.session_id(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue