Compare commits
169 Commits
v7.2.1
...
aarch-wind
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6688bcb488 | ||
|
|
73097075d4 | ||
|
|
5e1cab2e75 | ||
|
|
30f439caec | ||
|
|
4b5db4e5a9 | ||
|
|
3fa1d6b09c | ||
|
|
6051a5f1f1 | ||
|
|
5f7dd45361 | ||
|
|
3ca975d81d | ||
|
|
7e2bab921a | ||
|
|
a7b45ee859 | ||
|
|
62f4a1f3c2 | ||
|
|
3d4e387d35 | ||
|
|
2c46640105 | ||
|
|
41443d3989 | ||
|
|
4b3470d30b | ||
|
|
ce71a3df4d | ||
|
|
7232721b87 | ||
|
|
b2f133e6f1 | ||
|
|
87920964d7 | ||
|
|
c7a4f892e3 | ||
|
|
8f9e307930 | ||
|
|
856d43ce69 | ||
|
|
26181a5a1f | ||
|
|
edf87bf9a3 | ||
|
|
5f51538e0b | ||
|
|
48bb760739 | ||
|
|
42121a9fc9 | ||
|
|
02d79a4749 | ||
|
|
ddb26c73bd | ||
|
|
8ff064fe28 | ||
|
|
cf25eb0833 | ||
|
|
5c4260f3ac | ||
|
|
7abf19804c | ||
|
|
df707d5bef | ||
|
|
f3b0b0027e | ||
|
|
106a01223e | ||
|
|
506a60d0be | ||
|
|
4018607b77 | ||
|
|
1cc720ad34 | ||
|
|
73828af715 | ||
|
|
c0a96e3811 | ||
|
|
9d0d76f0a5 | ||
|
|
c7173ac832 | ||
|
|
b12ce47e7e | ||
|
|
269272983b | ||
|
|
6adc5eb9fb | ||
|
|
fd8bc8ae3f | ||
|
|
3369b0d429 | ||
|
|
097d39e8ce | ||
|
|
11fa08e6d6 | ||
|
|
96987af71d | ||
|
|
23327cb3ef | ||
|
|
b43b45be8f | ||
|
|
41994ec82e | ||
|
|
cd14ea7c62 | ||
|
|
9f13bca6b8 | ||
|
|
f4252c3e97 | ||
|
|
6598867d3d | ||
|
|
f39e040a0d | ||
|
|
a3d140269b | ||
|
|
feac29ea40 | ||
|
|
834c8cdbca | ||
|
|
d441fbdf91 | ||
|
|
e897f524dc | ||
|
|
1caf9446d8 | ||
|
|
bfd2c885db | ||
|
|
f467fa4e59 | ||
|
|
41fca4a2bb | ||
|
|
d38f955144 | ||
|
|
010e50a25d | ||
|
|
eab7c607cd | ||
|
|
3cafbf7f1a | ||
|
|
d7277b5a5b | ||
|
|
bb8dd1402d | ||
|
|
539cd0d418 | ||
|
|
0f8e1625d5 | ||
|
|
840e9bedb2 | ||
|
|
e11ad476fc | ||
|
|
c43726bc75 | ||
|
|
c9ab933a23 | ||
|
|
066a0b1668 | ||
|
|
aa68fe412e | ||
|
|
d748ea7e40 | ||
|
|
a7a282078f | ||
|
|
2fad3b588a | ||
|
|
4cb5d4a9c5 | ||
|
|
5b22ef192e | ||
|
|
34024d8524 | ||
|
|
ecc31dea15 | ||
|
|
d0e48d9bdc | ||
|
|
f6fc5599c0 | ||
|
|
89b6666436 | ||
|
|
94d45a2262 | ||
|
|
dc17a185ca | ||
|
|
4915477823 | ||
|
|
8662d2227c | ||
|
|
dd01a9bef3 | ||
|
|
6e320b1fd5 | ||
|
|
6e40993199 | ||
|
|
9d48af2b50 | ||
|
|
28d48a76e3 | ||
|
|
80eb14f9da | ||
|
|
623fa06d52 | ||
|
|
7154113c13 | ||
|
|
0a932ff880 | ||
|
|
7ef4a1ff12 | ||
|
|
ccc52b69d2 | ||
|
|
8139fdc738 | ||
|
|
a4fd53d516 | ||
|
|
27357110b5 | ||
|
|
fde78738b6 | ||
|
|
ce530e795a | ||
|
|
658d211779 | ||
|
|
66c1cd0d93 | ||
|
|
55ac231cec | ||
|
|
67674d53a2 | ||
|
|
8646b2dfce | ||
|
|
a2f68c2e3c | ||
|
|
5b1a090c5e | ||
|
|
e9efa238b0 | ||
|
|
0dabd8a1f6 | ||
|
|
b7a1f82f56 | ||
|
|
2507e096b7 | ||
|
|
b303b0a99c | ||
|
|
342fb57d14 | ||
|
|
a9ca77e27f | ||
|
|
6542304340 | ||
|
|
6b0f7f94b6 | ||
|
|
d87c76a23e | ||
|
|
305423b856 | ||
|
|
4b62190aff | ||
|
|
e17771a6a5 | ||
|
|
bac30ae78b | ||
|
|
c0219922b2 | ||
|
|
b5ed952d5c | ||
|
|
7994bc4909 | ||
|
|
b88d34c639 | ||
|
|
96cb1ee3fd | ||
|
|
003abe86bb | ||
|
|
6ec411a618 | ||
|
|
c7c0903804 | ||
|
|
cdc972a5ce | ||
|
|
17de912608 | ||
|
|
9876508887 | ||
|
|
72d62220e8 | ||
|
|
46ad337fa5 | ||
|
|
7a3ba7721f | ||
|
|
e0198e626b | ||
|
|
142705f386 | ||
|
|
4cb49c7825 | ||
|
|
05adb82dda | ||
|
|
faf7671799 | ||
|
|
d64db329dd | ||
|
|
e34d2339ad | ||
|
|
d196c5091c | ||
|
|
3e83f92532 | ||
|
|
41d7aaf323 | ||
|
|
e110f3726a | ||
|
|
eb5c897ac0 | ||
|
|
e864cf0c7d | ||
|
|
565c12405e | ||
|
|
2a6a8b42a6 | ||
|
|
5cb4cc0d1d | ||
|
|
62eb4f026f | ||
|
|
411d1a89c1 | ||
|
|
6ae0bf366a | ||
|
|
178cdc9dfa | ||
|
|
5bf1f86886 |
@@ -24,3 +24,6 @@ insert_final_newline = true
|
|||||||
|
|
||||||
[*.lua]
|
[*.lua]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.luau]
|
||||||
|
indent_style = tab
|
||||||
2
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# stylua formatting
|
||||||
|
0f8e1625d572a5fe0f7b5c08653ff92cc837d346
|
||||||
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.lua linguist-language=Luau
|
||||||
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
|||||||
patreon: lpghatguy
|
|
||||||
23
.github/workflows/changelog.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: Changelog Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Check Actions
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Changelog check
|
||||||
|
uses: Zomzog/changelog-checker@v1.3.0
|
||||||
|
with:
|
||||||
|
fileName: CHANGELOG.md
|
||||||
|
noChangelogLabel: skip changelog
|
||||||
|
checkNotification: Simple
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
66
.github/workflows/ci.yml
vendored
@@ -12,23 +12,27 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build and Test
|
name: Build and Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
rust_version: [stable, 1.57.0]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Setup Aftman
|
||||||
|
uses: ok-nick/setup-aftman@v0.3.0
|
||||||
with:
|
with:
|
||||||
toolchain: ${{ matrix.rust_version }}
|
version: 'v0.2.7'
|
||||||
override: true
|
|
||||||
profile: minimal
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --locked --verbose
|
run: cargo build --locked --verbose
|
||||||
@@ -36,24 +40,60 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --locked --verbose
|
run: cargo test --locked --verbose
|
||||||
|
|
||||||
lint:
|
msrv:
|
||||||
name: Rustfmt and Clippy
|
name: Check MSRV
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@1.70.0
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Setup Aftman
|
||||||
|
uses: ok-nick/setup-aftman@v0.3.0
|
||||||
|
with:
|
||||||
|
version: 'v0.2.7'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --locked --verbose
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Rustfmt, Clippy, Stylua, & Selene
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Setup Aftman
|
||||||
|
uses: ok-nick/setup-aftman@v0.3.0
|
||||||
|
with:
|
||||||
|
version: 'v0.2.7'
|
||||||
|
|
||||||
|
- name: Stylua
|
||||||
|
run: stylua --check plugin/src
|
||||||
|
|
||||||
|
- name: Selene
|
||||||
|
run: selene plugin/src
|
||||||
|
|
||||||
- name: Rustfmt
|
- name: Rustfmt
|
||||||
run: cargo fmt -- --check
|
run: cargo fmt -- --check
|
||||||
|
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy
|
run: cargo clippy
|
||||||
|
|
||||||
|
|||||||
116
.github/workflows/release.yml
vendored
@@ -8,49 +8,41 @@ jobs:
|
|||||||
create-release:
|
create-release:
|
||||||
name: Create Release
|
name: Create Release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
run: |
|
||||||
tag_name: ${{ github.ref }}
|
gh release create ${{ github.ref_name }} --draft --verify-tag --title ${{ github.ref_name }}
|
||||||
release_name: ${{ github.ref }}
|
|
||||||
draft: true
|
|
||||||
prerelease: false
|
|
||||||
|
|
||||||
build-plugin:
|
build-plugin:
|
||||||
needs: ["create-release"]
|
needs: ["create-release"]
|
||||||
name: Build Roblox Studio Plugin
|
name: Build Roblox Studio Plugin
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Setup Foreman
|
- name: Setup Aftman
|
||||||
uses: Roblox/setup-foreman@v1
|
uses: ok-nick/setup-aftman@v0.1.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
trust-check: false
|
||||||
|
version: 'v0.2.6'
|
||||||
|
|
||||||
- name: Build Plugin
|
- name: Build Plugin
|
||||||
run: rojo build plugin --output Rojo.rbxm
|
run: rojo build plugin --output Rojo.rbxm
|
||||||
|
|
||||||
- name: Upload Plugin to Release
|
- name: Upload Plugin to Release
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
run: |
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
gh release upload ${{ github.ref_name }} Rojo.rbxm
|
||||||
asset_path: Rojo.rbxm
|
|
||||||
asset_name: Rojo.rbxm
|
|
||||||
asset_content_type: application/octet-stream
|
|
||||||
|
|
||||||
- name: Upload Plugin to Artifacts
|
- name: Upload Plugin to Artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Rojo.rbxm
|
name: Rojo.rbxm
|
||||||
path: Rojo.rbxm
|
path: Rojo.rbxm
|
||||||
@@ -61,25 +53,31 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# https://doc.rust-lang.org/rustc/platform-support.html
|
# https://doc.rust-lang.org/rustc/platform-support.html
|
||||||
#
|
|
||||||
# FIXME: After the Rojo VS Code extension updates, add architecture
|
|
||||||
# names to each of these releases. We'll rename win64 to windows and add
|
|
||||||
# -x86_64 to each release.
|
|
||||||
include:
|
include:
|
||||||
- host: linux
|
- host: linux
|
||||||
os: ubuntu-18.04
|
os: ubuntu-20.04
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
label: linux
|
label: linux-x86_64
|
||||||
|
|
||||||
|
- host: linux
|
||||||
|
os: ubuntu-20.04
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
label: linux-aarch64
|
||||||
|
|
||||||
- host: windows
|
- host: windows
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
label: win64
|
label: windows-x86_64
|
||||||
|
|
||||||
|
- host: windows
|
||||||
|
os: windows-latest
|
||||||
|
target: aarch64-pc-windows-msvc
|
||||||
|
label: windows-aarch64
|
||||||
|
|
||||||
- host: macos
|
- host: macos
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
label: macos
|
label: macos-x86_64
|
||||||
|
|
||||||
- host: macos
|
- host: macos
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
@@ -91,63 +89,57 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
BIN: rojo
|
BIN: rojo
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Get Version from Tag
|
|
||||||
shell: bash
|
|
||||||
# https://github.community/t/how-to-get-just-the-tag-name/16241/7#M1027
|
|
||||||
run: |
|
|
||||||
echo "PROJECT_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
|
||||||
echo "Version is: ${{ env.PROJECT_VERSION }}"
|
|
||||||
|
|
||||||
- name: Install Rust
|
- name: Install Rust
|
||||||
uses: actions-rs/toolchain@v1
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
targets: ${{ matrix.target }}
|
||||||
target: ${{ matrix.target }}
|
|
||||||
override: true
|
- name: Setup Aftman
|
||||||
profile: minimal
|
uses: ok-nick/setup-aftman@v0.1.0
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
trust-check: false
|
||||||
|
version: 'v0.2.6'
|
||||||
|
|
||||||
- name: Build Release
|
- name: Build Release
|
||||||
run: cargo build --release --locked --verbose
|
run: cargo build --release --locked --verbose --target ${{ matrix.target }}
|
||||||
env:
|
env:
|
||||||
# Build into a known directory so we can find our build artifact more
|
# Build into a known directory so we can find our build artifact more
|
||||||
# easily.
|
# easily.
|
||||||
CARGO_TARGET_DIR: output
|
CARGO_TARGET_DIR: output
|
||||||
|
|
||||||
# On platforms that use OpenSSL, ensure it is statically linked to
|
- name: Generate Artifact Name
|
||||||
# make binaries more portable.
|
|
||||||
OPENSSL_STATIC: 1
|
|
||||||
|
|
||||||
- name: Create Release Archive
|
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAG_NAME: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
echo "ARTIFACT_NAME=$BIN-${TAG_NAME#v}-${{ matrix.label }}.zip" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Create Archive and Upload to Release
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
mkdir staging
|
mkdir staging
|
||||||
|
|
||||||
if [ "${{ matrix.host }}" = "windows" ]; then
|
if [ "${{ matrix.host }}" = "windows" ]; then
|
||||||
cp "output/release/$BIN.exe" staging/
|
cp "output/${{ matrix.target }}/release/$BIN.exe" staging/
|
||||||
cd staging
|
cd staging
|
||||||
7z a ../release.zip *
|
7z a ../$ARTIFACT_NAME *
|
||||||
else
|
else
|
||||||
cp "output/release/$BIN" staging/
|
cp "output/${{ matrix.target }}/release/$BIN" staging/
|
||||||
cd staging
|
cd staging
|
||||||
zip ../release.zip *
|
zip ../$ARTIFACT_NAME *
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload Archive to Release
|
gh release upload ${{ github.ref_name }} ../$ARTIFACT_NAME
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: release.zip
|
|
||||||
asset_name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
|
|
||||||
asset_content_type: application/octet-stream
|
|
||||||
|
|
||||||
- name: Upload Archive to Artifacts
|
- name: Upload Archive to Artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
|
path: ${{ env.ARTIFACT_NAME }}
|
||||||
path: release.zip
|
name: ${{ env.ARTIFACT_NAME }}
|
||||||
|
|||||||
31
.gitmodules
vendored
@@ -1,15 +1,18 @@
|
|||||||
[submodule "plugin/modules/roact"]
|
[submodule "plugin/Packages/Roact"]
|
||||||
path = plugin/modules/roact
|
path = plugin/Packages/Roact
|
||||||
url = https://github.com/Roblox/roact.git
|
url = https://github.com/roblox/roact.git
|
||||||
[submodule "plugin/modules/testez"]
|
[submodule "plugin/Packages/Flipper"]
|
||||||
path = plugin/modules/testez
|
path = plugin/Packages/Flipper
|
||||||
url = https://github.com/Roblox/testez.git
|
url = https://github.com/reselim/flipper.git
|
||||||
[submodule "plugin/modules/promise"]
|
[submodule "plugin/Packages/Promise"]
|
||||||
path = plugin/modules/promise
|
path = plugin/Packages/Promise
|
||||||
url = https://github.com/LPGhatguy/roblox-lua-promise.git
|
url = https://github.com/evaera/roblox-lua-promise.git
|
||||||
[submodule "plugin/modules/t"]
|
[submodule "plugin/Packages/t"]
|
||||||
path = plugin/modules/t
|
path = plugin/Packages/t
|
||||||
url = https://github.com/osyrisrblx/t.git
|
url = https://github.com/osyrisrblx/t.git
|
||||||
[submodule "plugin/modules/flipper"]
|
[submodule "plugin/Packages/TestEZ"]
|
||||||
path = plugin/modules/flipper
|
path = plugin/Packages/TestEZ
|
||||||
url = https://github.com/Reselim/Flipper
|
url = https://github.com/roblox/testez.git
|
||||||
|
[submodule "plugin/Packages/Highlighter"]
|
||||||
|
path = plugin/Packages/Highlighter
|
||||||
|
url = https://github.com/boatbomber/highlighter.git
|
||||||
|
|||||||
345
CHANGELOG.md
@@ -1,6 +1,349 @@
|
|||||||
# Rojo Changelog
|
# Rojo Changelog
|
||||||
|
|
||||||
## Unreleased Changes
|
## Unreleased Changes
|
||||||
|
* Projects may now manually link `Ref` properties together using `Attributes`. ([#843])
|
||||||
|
This has two parts: using `id` or `$id` in JSON files or a `Rojo_Target` attribute, an Instance
|
||||||
|
is given an ID. Then, that ID may be used elsewhere in the project to point to an Instance
|
||||||
|
using an attribute named `Rojo_Target_PROP_NAME`, where `PROP_NAME` is the name of a property.
|
||||||
|
|
||||||
|
As an example, here is a `model.json` for an ObjectValue that refers to itself:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "arbitrary string",
|
||||||
|
"attributes": {
|
||||||
|
"Rojo_Target_Value": "arbitrary string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a very rough implementation and the usage will become more ergonomic
|
||||||
|
over time.
|
||||||
|
|
||||||
|
* Updated Undo/Redo history to be more robust ([#915])
|
||||||
|
* Added popout diff visualizer for table properties like Attributes and Tags ([#834])
|
||||||
|
* Updated Theme to use Studio colors ([#838])
|
||||||
|
* Improved patch visualizer UX ([#883])
|
||||||
|
* Added update notifications for newer compatible versions in the Studio plugin. ([#832])
|
||||||
|
* Added experimental setting for Auto Connect in playtests ([#840])
|
||||||
|
* Improved settings UI ([#886])
|
||||||
|
* `Open Scripts Externally` option can now be changed while syncing ([#911])
|
||||||
|
* Projects may now specify rules for syncing files as if they had a different file extension. ([#813])
|
||||||
|
This is specified via a new field on project files, `syncRules`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"syncRules": [
|
||||||
|
{
|
||||||
|
"pattern": "*.foo",
|
||||||
|
"use": "text",
|
||||||
|
"exclude": "*.exclude.foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "*.bar.baz",
|
||||||
|
"use": "json",
|
||||||
|
"suffix": ".bar.baz",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "SyncRulesAreCool",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `pattern` field is a glob used to match the sync rule to files. If present, the `suffix` field allows you to specify parts of a file's name get cut off by Rojo to name the Instance, including the file extension. If it isn't specified, Rojo will only cut off the first part of the file extension, up to the first dot.
|
||||||
|
|
||||||
|
Additionally, the `exclude` field allows files to be excluded from the sync rule if they match a pattern specified by it. If it's not present, all files that match `pattern` will be modified using the sync rule.
|
||||||
|
|
||||||
|
The `use` field corresponds to one of the potential file type that Rojo will currently include in a project. Files that match the provided pattern will be treated as if they had the file extension for that file type. A full list is below:
|
||||||
|
|
||||||
|
| `use` value | file extension |
|
||||||
|
|:---------------|:----------------|
|
||||||
|
| `serverScript` | `.server.lua` |
|
||||||
|
| `clientScript` | `.client.lua` |
|
||||||
|
| `moduleScript` | `.lua` |
|
||||||
|
| `json` | `.json` |
|
||||||
|
| `toml` | `.toml` |
|
||||||
|
| `csv` | `.csv` |
|
||||||
|
| `text` | `.txt` |
|
||||||
|
| `jsonModel` | `.model.json` |
|
||||||
|
| `rbxm` | `.rbxm` |
|
||||||
|
| `rbxmx` | `.rbxmx` |
|
||||||
|
| `project` | `.project.json` |
|
||||||
|
| `ignore` | None! |
|
||||||
|
|
||||||
|
**All** sync rules are reset between project files, so they must be specified in each one when nesting them. This is to ensure that nothing can break other projects by changing how files are synced!
|
||||||
|
|
||||||
|
[#813]: https://github.com/rojo-rbx/rojo/pull/813
|
||||||
|
[#832]: https://github.com/rojo-rbx/rojo/pull/832
|
||||||
|
[#834]: https://github.com/rojo-rbx/rojo/pull/834
|
||||||
|
[#838]: https://github.com/rojo-rbx/rojo/pull/838
|
||||||
|
[#840]: https://github.com/rojo-rbx/rojo/pull/840
|
||||||
|
[#843]: https://github.com/rojo-rbx/rojo/pull/843
|
||||||
|
[#883]: https://github.com/rojo-rbx/rojo/pull/883
|
||||||
|
[#886]: https://github.com/rojo-rbx/rojo/pull/886
|
||||||
|
[#911]: https://github.com/rojo-rbx/rojo/pull/911
|
||||||
|
[#915]: https://github.com/rojo-rbx/rojo/pull/915
|
||||||
|
|
||||||
|
## [7.4.3] - August 6th, 2024
|
||||||
|
* Fixed issue with building binary files introduced in 7.4.2
|
||||||
|
* Fixed `value of type nil cannot be converted to number` warning spam in output. [#955]
|
||||||
|
|
||||||
|
[#955]: https://github.com/rojo-rbx/rojo/pull/955
|
||||||
|
|
||||||
|
## [7.4.2] - July 23, 2024
|
||||||
|
* Added Never option to Confirmation ([#893])
|
||||||
|
* Fixed removing trailing newlines ([#903])
|
||||||
|
* Updated the internal property database, correcting an issue with `SurfaceAppearance.Color` that was reported [here][Surface_Appearance_Color_1] and [here][Surface_Appearance_Color_2] ([#948])
|
||||||
|
|
||||||
|
[#893]: https://github.com/rojo-rbx/rojo/pull/893
|
||||||
|
[#903]: https://github.com/rojo-rbx/rojo/pull/903
|
||||||
|
[#948]: https://github.com/rojo-rbx/rojo/pull/948
|
||||||
|
[Surface_Appearance_Color_1]: https://devforum.roblox.com/t/jailbreak-custom-character-turned-shiny-black-no-texture/3075563
|
||||||
|
[Surface_Appearance_Color_2]: https://devforum.roblox.com/t/surfaceappearance-not-displaying-correctly/3075588
|
||||||
|
|
||||||
|
## [7.4.1] - February 20, 2024
|
||||||
|
* Made the `name` field optional on project files ([#870])
|
||||||
|
|
||||||
|
Files named `default.project.json` inherit the name of the folder they're in and all other projects
|
||||||
|
are named as expect (e.g. `foo.project.json` becomes an Instance named `foo`)
|
||||||
|
|
||||||
|
There is no change in behavior if `name` is set.
|
||||||
|
* Fixed incorrect results when building model pivots ([#865])
|
||||||
|
* Fixed incorrect results when serving model pivots ([#868])
|
||||||
|
* Rojo now converts any line endings to LF, preventing spurious diffs when syncing Lua files on Windows ([#854])
|
||||||
|
* Fixed Rojo plugin failing to connect when project contains certain unreadable properties ([#848])
|
||||||
|
* Fixed various cases where patch visualizer would not display sync failures ([#845], [#844])
|
||||||
|
* Fixed http error handling so Rojo can be used in Github Codespaces ([#847])
|
||||||
|
|
||||||
|
[#848]: https://github.com/rojo-rbx/rojo/pull/848
|
||||||
|
[#845]: https://github.com/rojo-rbx/rojo/pull/845
|
||||||
|
[#844]: https://github.com/rojo-rbx/rojo/pull/844
|
||||||
|
[#847]: https://github.com/rojo-rbx/rojo/pull/847
|
||||||
|
[#854]: https://github.com/rojo-rbx/rojo/pull/854
|
||||||
|
[#865]: https://github.com/rojo-rbx/rojo/pull/865
|
||||||
|
[#868]: https://github.com/rojo-rbx/rojo/pull/868
|
||||||
|
[#870]: https://github.com/rojo-rbx/rojo/pull/870
|
||||||
|
|
||||||
|
## [7.4.0] - January 16, 2024
|
||||||
|
* Improved the visualization for array properties like Tags ([#829])
|
||||||
|
* Significantly improved performance of `rojo serve`, `rojo build --watch`, and `rojo sourcemap --watch` on macOS. ([#830])
|
||||||
|
* Changed *.lua files that init command generates to *.luau ([#831])
|
||||||
|
* Does not remind users to sync if the sync lock is claimed already ([#833])
|
||||||
|
|
||||||
|
[#829]: https://github.com/rojo-rbx/rojo/pull/829
|
||||||
|
[#830]: https://github.com/rojo-rbx/rojo/pull/830
|
||||||
|
[#831]: https://github.com/rojo-rbx/rojo/pull/831
|
||||||
|
[#833]: https://github.com/rojo-rbx/rojo/pull/833
|
||||||
|
|
||||||
|
## [7.4.0-rc3] - October 25, 2023
|
||||||
|
* Changed `sourcemap --watch` to only generate the sourcemap when it's necessary ([#800])
|
||||||
|
* Switched script source property getter and setter to `ScriptEditorService` methods ([#801])
|
||||||
|
|
||||||
|
This ensures that the script editor reflects any changes Rojo makes to a script while it is open in the script editor.
|
||||||
|
|
||||||
|
* Fixed issues when handling `SecurityCapabilities` values ([#803], [#807])
|
||||||
|
* Fixed Rojo plugin erroring out when attempting to sync attributes with invalid names ([#809])
|
||||||
|
|
||||||
|
[#800]: https://github.com/rojo-rbx/rojo/pull/800
|
||||||
|
[#801]: https://github.com/rojo-rbx/rojo/pull/801
|
||||||
|
[#803]: https://github.com/rojo-rbx/rojo/pull/803
|
||||||
|
[#807]: https://github.com/rojo-rbx/rojo/pull/807
|
||||||
|
[#809]: https://github.com/rojo-rbx/rojo/pull/809
|
||||||
|
|
||||||
|
## [7.4.0-rc2] - October 3, 2023
|
||||||
|
* Fixed bug with parsing version for plugin validation ([#797])
|
||||||
|
|
||||||
|
[#797]: https://github.com/rojo-rbx/rojo/pull/797
|
||||||
|
|
||||||
|
## [7.4.0-rc1] - October 3, 2023
|
||||||
|
### Additions
|
||||||
|
#### Project format
|
||||||
|
* Added support for `.toml` files to `$path` ([#633])
|
||||||
|
* Added support for `Font` and `CFrame` attributes ([rbx-dom#299], [rbx-dom#296])
|
||||||
|
* Added the `emitLegacyScripts` field to the project format ([#765]). The behavior is outlined below:
|
||||||
|
|
||||||
|
| `emitLegacyScripts` Value | Action Taken by Rojo |
|
||||||
|
|---------------------------|------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| false | Rojo emits Scripts with the appropriate `RunContext` for `*.client.lua` and `*.server.lua` files in the project. |
|
||||||
|
| true (default) | Rojo emits LocalScripts and Scripts with legacy `RunContext` (same behavior as previously). |
|
||||||
|
|
||||||
|
|
||||||
|
It can be used like this:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"emitLegacyScripts": false,
|
||||||
|
"name": "MyCoolRunContextProject",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* Added `Terrain` classname inference, similar to services ([#771])
|
||||||
|
|
||||||
|
`Terrain` may now be defined in projects without using `$className`:
|
||||||
|
```json
|
||||||
|
"Workspace": {
|
||||||
|
"Terrain": {
|
||||||
|
"$path": "path/to/terrain.rbxm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* Added support for `Terrain.MaterialColors` ([#770])
|
||||||
|
|
||||||
|
`Terrain.MaterialColors` is now represented in projects in a human readable format:
|
||||||
|
```json
|
||||||
|
"Workspace": {
|
||||||
|
"Terrain": {
|
||||||
|
"$path": "path/to/terrain.rbxm"
|
||||||
|
"$properties": {
|
||||||
|
"MaterialColors": {
|
||||||
|
"Grass": [10, 20, 30],
|
||||||
|
"Asphalt": [40, 50, 60],
|
||||||
|
"LeafyGrass": [255, 155, 55]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* Added better support for `Font` properties ([#731])
|
||||||
|
|
||||||
|
`FontFace` properties may now be defined using implicit property syntax:
|
||||||
|
```json
|
||||||
|
"TextBox": {
|
||||||
|
"$className": "TextBox",
|
||||||
|
"$properties": {
|
||||||
|
"FontFace": {
|
||||||
|
"family": "rbxasset://fonts/families/RobotoMono.json",
|
||||||
|
"weight": "Thin",
|
||||||
|
"style": "Normal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Patch visualizer and notifications
|
||||||
|
* Added a setting to control patch confirmation behavior ([#774])
|
||||||
|
|
||||||
|
This is a new setting for controlling when the Rojo plugin prompts for confirmation before syncing. It has four options:
|
||||||
|
* Initial (default): prompts only once for a project in a given Studio session
|
||||||
|
* Always: always prompts for confirmation
|
||||||
|
* Large Changes: only prompts when there are more than X changed instances. The number of instances is configurable - an additional setting for the number of instances becomes available when this option is chosen
|
||||||
|
* Unlisted PlaceId: only prompts if the place ID is not present in servePlaceIds
|
||||||
|
|
||||||
|
* Added the ability to select Instances in patch visualizer ([#709])
|
||||||
|
|
||||||
|
Double-clicking an instance in the patch visualizer sets Roblox Studio's selection to the instance.
|
||||||
|
|
||||||
|
* Added a sync reminder notification. ([#689])
|
||||||
|
|
||||||
|
Rojo detects if you have previously synced to a place, and displays a notification reminding you to sync again:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
* Added rich Source diffs in patch visualizer ([#748])
|
||||||
|
|
||||||
|
A "View Diff" button for script sources is now present in the patch visualizer. Clicking it displays a side-by-side diff of the script changes:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
* Patch visualizer now indicates what changes failed to apply. ([#717])
|
||||||
|
|
||||||
|
A clickable warning label is displayed when the Rojo plugin is unable to apply changes. Clicking the label displays precise information about which changes failed:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
#### Miscellaneous
|
||||||
|
* Added `plugin` flag to the `build` command that outputs to the local plugins folder ([#735])
|
||||||
|
|
||||||
|
This is a flag that builds a Rojo project into Roblox Studio's plugins directory. This allows you to build a Rojo project and load it into Studio as a plugin without having to type the full path to the plugins directory. It can be used like this: `rojo build <PATH-TO-PROJECT> --plugin <FILE-NAME>`
|
||||||
|
|
||||||
|
* Added new plugin template to the `init` command ([#738])
|
||||||
|
|
||||||
|
This is a new template geared towards plugins. It is similar to the model template, but creates a `Script` instead of a `ModuleScript` in the `src` directory. It can be used like this: `rojo init --kind plugin`
|
||||||
|
|
||||||
|
* Added protection against syncing non-place projects as a place. ([#691])
|
||||||
|
* Add buttons for navigation on the Connected page ([#722])
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
* Significantly improved performance of `rojo sourcemap` ([#668])
|
||||||
|
* Fixed the diff visualizer of connected sessions. ([#674])
|
||||||
|
* Fixed disconnected session activity. ([#675])
|
||||||
|
* Skip confirming patches that contain only a datamodel name change. ([#688])
|
||||||
|
* Fix Rojo breaking when users undo/redo in Studio ([#708])
|
||||||
|
* Improve tooltip behavior ([#723])
|
||||||
|
* Better settings controls ([#725])
|
||||||
|
* Rework patch visualizer with many fixes and improvements ([#713], [#726], [#755])
|
||||||
|
|
||||||
|
[#668]: https://github.com/rojo-rbx/rojo/pull/668
|
||||||
|
[#674]: https://github.com/rojo-rbx/rojo/pull/674
|
||||||
|
[#675]: https://github.com/rojo-rbx/rojo/pull/675
|
||||||
|
[#688]: https://github.com/rojo-rbx/rojo/pull/688
|
||||||
|
[#689]: https://github.com/rojo-rbx/rojo/pull/689
|
||||||
|
[#691]: https://github.com/rojo-rbx/rojo/pull/691
|
||||||
|
[#709]: https://github.com/rojo-rbx/rojo/pull/709
|
||||||
|
[#708]: https://github.com/rojo-rbx/rojo/pull/708
|
||||||
|
[#713]: https://github.com/rojo-rbx/rojo/pull/713
|
||||||
|
[#717]: https://github.com/rojo-rbx/rojo/pull/717
|
||||||
|
[#722]: https://github.com/rojo-rbx/rojo/pull/722
|
||||||
|
[#723]: https://github.com/rojo-rbx/rojo/pull/723
|
||||||
|
[#725]: https://github.com/rojo-rbx/rojo/pull/725
|
||||||
|
[#726]: https://github.com/rojo-rbx/rojo/pull/726
|
||||||
|
[#633]: https://github.com/rojo-rbx/rojo/pull/633
|
||||||
|
[#735]: https://github.com/rojo-rbx/rojo/pull/735
|
||||||
|
[#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
|
||||||
|
[#765]: https://github.com/rojo-rbx/rojo/pull/765
|
||||||
|
[#770]: https://github.com/rojo-rbx/rojo/pull/770
|
||||||
|
[#771]: https://github.com/rojo-rbx/rojo/pull/771
|
||||||
|
[#774]: https://github.com/rojo-rbx/rojo/pull/774
|
||||||
|
[rbx-dom#299]: https://github.com/rojo-rbx/rbx-dom/pull/299
|
||||||
|
[rbx-dom#296]: https://github.com/rojo-rbx/rbx-dom/pull/296
|
||||||
|
|
||||||
|
## [7.3.0] - April 22, 2023
|
||||||
|
* Added `$attributes` to project format. ([#574])
|
||||||
|
* Added `--watch` flag to `rojo sourcemap`. ([#602])
|
||||||
|
* Added support for `init.csv` files. ([#594])
|
||||||
|
* Added real-time sync status to the Studio plugin. ([#569])
|
||||||
|
* Added support for copying error messages to the clipboard. ([#614])
|
||||||
|
* Added sync locking for Team Create. ([#590])
|
||||||
|
* Added support for specifying HTTP or HTTPS protocol in plugin. ([#642])
|
||||||
|
* Added tooltips to buttons in the Studio plugin. ([#637])
|
||||||
|
* Added visual diffs when connecting from the Studio plugin. ([#603])
|
||||||
|
* Host and port are now saved in the Studio plugin. ([#613])
|
||||||
|
* Improved padding on notifications in Studio plugin. ([#589])
|
||||||
|
* Renamed `Common` to `Shared` in the default Rojo project. ([#611])
|
||||||
|
* Reduced the minimum size of the Studio plugin widget. ([#606])
|
||||||
|
* Fixed current directory in `rojo fmt-project`. ([#581])
|
||||||
|
* Fixed errors after a session has already ended. ([#587])
|
||||||
|
* Fixed an uncommon security permission error ([#619])
|
||||||
|
|
||||||
|
[#569]: https://github.com/rojo-rbx/rojo/pull/569
|
||||||
|
[#574]: https://github.com/rojo-rbx/rojo/pull/574
|
||||||
|
[#581]: https://github.com/rojo-rbx/rojo/pull/581
|
||||||
|
[#587]: https://github.com/rojo-rbx/rojo/pull/587
|
||||||
|
[#589]: https://github.com/rojo-rbx/rojo/pull/589
|
||||||
|
[#590]: https://github.com/rojo-rbx/rojo/pull/590
|
||||||
|
[#594]: https://github.com/rojo-rbx/rojo/pull/594
|
||||||
|
[#602]: https://github.com/rojo-rbx/rojo/pull/602
|
||||||
|
[#603]: https://github.com/rojo-rbx/rojo/pull/603
|
||||||
|
[#606]: https://github.com/rojo-rbx/rojo/pull/606
|
||||||
|
[#611]: https://github.com/rojo-rbx/rojo/pull/611
|
||||||
|
[#613]: https://github.com/rojo-rbx/rojo/pull/613
|
||||||
|
[#614]: https://github.com/rojo-rbx/rojo/pull/614
|
||||||
|
[#619]: https://github.com/rojo-rbx/rojo/pull/619
|
||||||
|
[#637]: https://github.com/rojo-rbx/rojo/pull/637
|
||||||
|
[#642]: https://github.com/rojo-rbx/rojo/pull/642
|
||||||
|
[7.3.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.3.0
|
||||||
|
|
||||||
## [7.2.1] - July 8, 2022
|
## [7.2.1] - July 8, 2022
|
||||||
* Fixed notification sound by changing it to a generic sound. ([#566])
|
* Fixed notification sound by changing it to a generic sound. ([#566])
|
||||||
@@ -8,7 +351,7 @@
|
|||||||
|
|
||||||
[#566]: https://github.com/rojo-rbx/rojo/pull/566
|
[#566]: https://github.com/rojo-rbx/rojo/pull/566
|
||||||
[#568]: https://github.com/rojo-rbx/rojo/pull/568
|
[#568]: https://github.com/rojo-rbx/rojo/pull/568
|
||||||
[7.2.0]: https://github.com/rojo-rbx/rojo/releases/tag/v7.2.0
|
[7.2.1]: https://github.com/rojo-rbx/rojo/releases/tag/v7.2.1
|
||||||
|
|
||||||
## [7.2.0] - June 29, 2022
|
## [7.2.0] - June 29, 2022
|
||||||
* Added support for `.luau` files. ([#552])
|
* Added support for `.luau` files. ([#552])
|
||||||
|
|||||||
1611
Cargo.lock
generated
96
Cargo.toml
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rojo"
|
name = "rojo"
|
||||||
version = "7.2.1"
|
version = "7.4.0"
|
||||||
rust-version = "1.57.0"
|
rust-version = "1.70.0"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
description = "Enables professional-grade development tools for Roblox developers"
|
description = "Enables professional-grade development tools for Roblox developers"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
@@ -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"
|
||||||
@@ -28,7 +26,9 @@ default = []
|
|||||||
# Enable this feature to live-reload assets from the web UI.
|
# Enable this feature to live-reload assets from the web UI.
|
||||||
dev_live_assets = []
|
dev_live_assets = []
|
||||||
|
|
||||||
profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client"]
|
# Run Rojo with this feature to open a Tracy session.
|
||||||
|
# Currently uses protocol v63, last supported in Tracy 0.9.1.
|
||||||
|
profile-with-tracy = ["profiling/profile-with-tracy"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
@@ -42,7 +42,7 @@ name = "build"
|
|||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
memofs = { version = "0.2.0", path = "crates/memofs" }
|
memofs = { version = "0.3.0", path = "crates/memofs" }
|
||||||
|
|
||||||
# These dependencies can be uncommented when working on rbx-dom simultaneously
|
# These dependencies can be uncommented when working on rbx-dom simultaneously
|
||||||
# rbx_binary = { path = "../rbx-dom/rbx_binary" }
|
# rbx_binary = { path = "../rbx-dom/rbx_binary" }
|
||||||
@@ -51,60 +51,66 @@ 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.6.5"
|
rbx_binary = "0.7.7"
|
||||||
rbx_dom_weak = "2.4.0"
|
rbx_dom_weak = "2.9.0"
|
||||||
rbx_reflection = "4.2.0"
|
rbx_reflection = "4.7.0"
|
||||||
rbx_reflection_database = "0.2.2"
|
rbx_reflection_database = "0.2.12"
|
||||||
rbx_xml = "0.12.3"
|
rbx_xml = "0.13.5"
|
||||||
|
|
||||||
anyhow = "1.0.44"
|
anyhow = "1.0.80"
|
||||||
backtrace = "0.3.61"
|
backtrace = "0.3.69"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
crossbeam-channel = "0.5.1"
|
crossbeam-channel = "0.5.12"
|
||||||
csv = "1.1.6"
|
csv = "1.3.0"
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.9.3"
|
||||||
fs-err = "2.6.0"
|
fs-err = "2.11.0"
|
||||||
futures = "0.3.17"
|
futures = "0.3.30"
|
||||||
globset = "0.4.8"
|
globset = "0.4.14"
|
||||||
humantime = "2.1.0"
|
humantime = "2.1.0"
|
||||||
hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] }
|
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] }
|
||||||
jod-thread = "0.1.2"
|
jod-thread = "0.1.2"
|
||||||
log = "0.4.14"
|
log = "0.4.21"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
notify = "4.0.17"
|
num_cpus = "1.16.0"
|
||||||
opener = "0.5.0"
|
opener = "0.5.2"
|
||||||
reqwest = { version = "0.11.10", features = ["blocking", "json"] }
|
rayon = "1.9.0"
|
||||||
|
reqwest = { version = "0.11.24", default-features = false, features = [
|
||||||
|
"blocking",
|
||||||
|
"json",
|
||||||
|
"rustls-tls",
|
||||||
|
] }
|
||||||
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.197", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0.68"
|
serde_json = "1.0.114"
|
||||||
termcolor = "1.1.2"
|
toml = "0.5.11"
|
||||||
thiserror = "1.0.30"
|
termcolor = "1.4.1"
|
||||||
tokio = { version = "1.12.0", features = ["rt", "rt-multi-thread"] }
|
thiserror = "1.0.57"
|
||||||
uuid = { version = "1.0.0", features = ["v4", "serde"] }
|
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread"] }
|
||||||
clap = { version = "3.1.18", features = ["derive"] }
|
uuid = { version = "1.7.0", features = ["v4", "serde"] }
|
||||||
profiling = "1.0.6"
|
clap = { version = "3.2.25", features = ["derive"] }
|
||||||
tracy-client = { version = "0.13.2", optional = true }
|
profiling = "1.0.15"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.10.1"
|
winreg = "0.10.1"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
memofs = { version = "0.2.0", path = "crates/memofs" }
|
memofs = { version = "0.3.0", path = "crates/memofs" }
|
||||||
|
|
||||||
embed-resource = "1.6.4"
|
embed-resource = "1.8.0"
|
||||||
anyhow = "1.0.44"
|
anyhow = "1.0.80"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
fs-err = "2.6.0"
|
fs-err = "2.11.0"
|
||||||
maplit = "1.0.2"
|
maplit = "1.0.2"
|
||||||
|
semver = "1.0.22"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rojo-insta-ext = { path = "crates/rojo-insta-ext" }
|
rojo-insta-ext = { path = "crates/rojo-insta-ext" }
|
||||||
|
|
||||||
criterion = "0.3.5"
|
criterion = "0.3.6"
|
||||||
insta = { version = "1.8.0", features = ["redactions"] }
|
insta = { version = "1.36.1", features = ["redactions", "yaml"] }
|
||||||
paste = "1.0.5"
|
paste = "1.0.14"
|
||||||
pretty_assertions = "1.2.1"
|
pretty_assertions = "1.4.0"
|
||||||
serde_yaml = "0.8.21"
|
serde_yaml = "0.8.26"
|
||||||
tempfile = "3.2.0"
|
tempfile = "3.10.1"
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.5.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://rojo.space"><img src="assets/logo-512.png" alt="Rojo" height="217" /></a>
|
<a href="https://rojo.space"><img src="assets/brand_images/logo-512.png" alt="Rojo" height="217" /></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div> </div>
|
<div> </div>
|
||||||
@@ -40,7 +40,7 @@ Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions fo
|
|||||||
|
|
||||||
Pull requests are welcome!
|
Pull requests are welcome!
|
||||||
|
|
||||||
Rojo supports Rust 1.57.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
Rojo supports Rust 1.70.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||||
5
aftman.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[tools]
|
||||||
|
rojo = "rojo-rbx/rojo@7.3.0"
|
||||||
|
selene = "Kampfkarren/selene@0.26.1"
|
||||||
|
stylua = "JohnnyMorganz/stylua@0.18.2"
|
||||||
|
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"
|
||||||
|
Before Width: | Height: | Size: 975 B After Width: | Height: | Size: 975 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
@@ -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"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"$className": "DataModel",
|
"$className": "DataModel",
|
||||||
|
|
||||||
"ReplicatedStorage": {
|
"ReplicatedStorage": {
|
||||||
"Common": {
|
"Shared": {
|
||||||
"$path": "src/shared"
|
"$path": "src/shared"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
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
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "{project_name}",
|
||||||
|
"tree": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
assets/default-plugin-project/gitignore.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Plugin model files
|
||||||
|
/{project_name}.rbxmx
|
||||||
|
/{project_name}.rbxm
|
||||||
|
Before Width: | Height: | Size: 229 B After Width: | Height: | Size: 229 B |
|
Before Width: | Height: | Size: 584 B After Width: | Height: | Size: 584 B |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
BIN
assets/images/icons/debug.png
Normal file
|
After Width: | Height: | Size: 183 B |
BIN
assets/images/icons/expand.png
Normal file
|
After Width: | Height: | Size: 273 B |
BIN
assets/images/icons/reset.png
Normal file
|
After Width: | Height: | Size: 933 B |
BIN
assets/images/icons/warning.png
Normal file
|
After Width: | Height: | Size: 241 B |
|
Before Width: | Height: | Size: 175 B After Width: | Height: | Size: 175 B |
BIN
assets/images/syncsuccess.png
Normal file
|
After Width: | Height: | Size: 574 B |
BIN
assets/images/syncwarning.png
Normal file
|
After Width: | Height: | Size: 607 B |
@@ -3,7 +3,7 @@ use std::path::Path;
|
|||||||
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||||
use tempfile::{tempdir, TempDir};
|
use tempfile::{tempdir, TempDir};
|
||||||
|
|
||||||
use librojo::cli::{build, BuildCommand};
|
use librojo::cli::BuildCommand;
|
||||||
|
|
||||||
pub fn benchmark_small_place(c: &mut Criterion) {
|
pub fn benchmark_small_place(c: &mut Criterion) {
|
||||||
bench_build_place(c, "Small Place", "test-projects/benchmark_small_place")
|
bench_build_place(c, "Small Place", "test-projects/benchmark_small_place")
|
||||||
@@ -20,7 +20,7 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
|
|||||||
group.bench_function("build", |b| {
|
group.bench_function("build", |b| {
|
||||||
b.iter_batched(
|
b.iter_batched(
|
||||||
|| place_setup(path),
|
|| place_setup(path),
|
||||||
|(_dir, options)| build(options).unwrap(),
|
|(_dir, options)| options.run().unwrap(),
|
||||||
BatchSize::SmallInput,
|
BatchSize::SmallInput,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -31,11 +31,12 @@ fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
|
|||||||
fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) {
|
fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let input = input_path.as_ref().to_path_buf();
|
let input = input_path.as_ref().to_path_buf();
|
||||||
let output = dir.path().join("output.rbxlx");
|
let output = Some(dir.path().join("output.rbxlx"));
|
||||||
|
|
||||||
let options = BuildCommand {
|
let options = BuildCommand {
|
||||||
project: input,
|
project: input,
|
||||||
watch: false,
|
watch: false,
|
||||||
|
plugin: None,
|
||||||
output,
|
output,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
28
build.rs
@@ -7,6 +7,7 @@ use fs_err as fs;
|
|||||||
use fs_err::File;
|
use fs_err::File;
|
||||||
use maplit::hashmap;
|
use maplit::hashmap;
|
||||||
use memofs::VfsSnapshot;
|
use memofs::VfsSnapshot;
|
||||||
|
use semver::Version;
|
||||||
|
|
||||||
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
|
||||||
println!("cargo:rerun-if-changed={}", path.display());
|
println!("cargo:rerun-if-changed={}", path.display());
|
||||||
@@ -43,7 +44,14 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
|
||||||
let plugin_root = PathBuf::from(root_dir).join("plugin");
|
let plugin_root = PathBuf::from(root_dir).join("plugin");
|
||||||
|
|
||||||
let plugin_modules = plugin_root.join("modules");
|
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
|
||||||
|
let plugin_version =
|
||||||
|
Version::parse(fs::read_to_string(plugin_root.join("Version.txt"))?.trim())?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
our_version, plugin_version,
|
||||||
|
"plugin version does not match Cargo version"
|
||||||
|
);
|
||||||
|
|
||||||
let snapshot = VfsSnapshot::dir(hashmap! {
|
let snapshot = VfsSnapshot::dir(hashmap! {
|
||||||
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
|
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
|
||||||
@@ -52,24 +60,12 @@ fn main() -> Result<(), anyhow::Error> {
|
|||||||
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
|
"log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
|
||||||
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
|
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
|
||||||
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
|
"src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
|
||||||
"modules" => VfsSnapshot::dir(hashmap! {
|
"Packages" => snapshot_from_fs_path(&plugin_root.join("Packages"))?,
|
||||||
"roact" => VfsSnapshot::dir(hashmap! {
|
"Version.txt" => snapshot_from_fs_path(&plugin_root.join("Version.txt"))?,
|
||||||
"src" => snapshot_from_fs_path(&plugin_modules.join("roact").join("src"))?
|
|
||||||
}),
|
|
||||||
"promise" => VfsSnapshot::dir(hashmap! {
|
|
||||||
"lib" => snapshot_from_fs_path(&plugin_modules.join("promise").join("lib"))?
|
|
||||||
}),
|
|
||||||
"t" => VfsSnapshot::dir(hashmap! {
|
|
||||||
"lib" => snapshot_from_fs_path(&plugin_modules.join("t").join("lib"))?
|
|
||||||
}),
|
|
||||||
"flipper" => VfsSnapshot::dir(hashmap! {
|
|
||||||
"src" => snapshot_from_fs_path(&plugin_modules.join("flipper").join("src"))?
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
let out_path = Path::new(&out_dir).join("plugin.bincode");
|
||||||
let out_file = File::create(&out_path)?;
|
let out_file = File::create(out_path)?;
|
||||||
|
|
||||||
bincode::serialize_into(out_file, &snapshot)?;
|
bincode::serialize_into(out_file, &snapshot)?;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
## Unreleased Changes
|
## Unreleased Changes
|
||||||
|
|
||||||
|
## 0.3.0 (2024-03-15)
|
||||||
|
* Changed `StdBackend` file watching component to use minimal recursive watches. [#830]
|
||||||
|
* Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854]
|
||||||
|
|
||||||
|
[#830]: https://github.com/rojo-rbx/rojo/pull/830
|
||||||
|
[#854]: https://github.com/rojo-rbx/rojo/pull/854
|
||||||
|
|
||||||
## 0.2.0 (2021-08-23)
|
## 0.2.0 (2021-08-23)
|
||||||
* Updated to `crossbeam-channel` 0.5.1.
|
* Updated to `crossbeam-channel` 0.5.1.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "memofs"
|
name = "memofs"
|
||||||
description = "Virtual filesystem with configurable backends."
|
description = "Virtual filesystem with configurable backends."
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -11,7 +11,7 @@ homepage = "https://github.com/rojo-rbx/rojo/tree/master/memofs"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
crossbeam-channel = "0.5.1"
|
crossbeam-channel = "0.5.12"
|
||||||
fs-err = "2.3.0"
|
fs-err = "2.11.0"
|
||||||
notify = "4.0.15"
|
notify = "4.0.17"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ impl InMemoryFs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for InMemoryFs {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct InMemoryFsInner {
|
struct InMemoryFsInner {
|
||||||
entries: HashMap<PathBuf, Entry>,
|
entries: HashMap<PathBuf, Entry>,
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ mod noop_backend;
|
|||||||
mod snapshot;
|
mod snapshot;
|
||||||
mod std_backend;
|
mod std_backend;
|
||||||
|
|
||||||
use std::io;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, Mutex, MutexGuard};
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
|
use std::{io, str};
|
||||||
|
|
||||||
pub use in_memory_fs::InMemoryFs;
|
pub use in_memory_fs::InMemoryFs;
|
||||||
pub use noop_backend::NoopBackend;
|
pub use noop_backend::NoopBackend;
|
||||||
@@ -155,6 +155,24 @@ impl VfsInner {
|
|||||||
Ok(Arc::new(contents))
|
Ok(Arc::new(contents))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_to_string<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Arc<String>> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let contents = self.backend.read(path)?;
|
||||||
|
|
||||||
|
if self.watch_enabled {
|
||||||
|
self.backend.watch(path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents_str = str::from_utf8(&contents).map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
format!("File was not valid UTF-8: {}", path.display()),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Arc::new(contents_str.into()))
|
||||||
|
}
|
||||||
|
|
||||||
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> {
|
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let contents = contents.as_ref();
|
let contents = contents.as_ref();
|
||||||
@@ -194,11 +212,8 @@ impl VfsInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
|
fn commit_event(&mut self, event: &VfsEvent) -> io::Result<()> {
|
||||||
match event {
|
if let VfsEvent::Remove(path) = event {
|
||||||
VfsEvent::Remove(path) => {
|
let _ = self.backend.unwatch(path);
|
||||||
let _ = self.backend.unwatch(&path);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -261,6 +276,33 @@ impl Vfs {
|
|||||||
self.inner.lock().unwrap().read(path)
|
self.inner.lock().unwrap().read(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read a file from the VFS (or from the underlying backend if it isn't
|
||||||
|
/// resident) into a string.
|
||||||
|
///
|
||||||
|
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string].
|
||||||
|
///
|
||||||
|
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
|
||||||
|
#[inline]
|
||||||
|
pub fn read_to_string<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
self.inner.lock().unwrap().read_to_string(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a file from the VFS (or the underlying backend if it isn't
|
||||||
|
/// resident) into a string, and normalize its line endings to LF.
|
||||||
|
///
|
||||||
|
/// Roughly equivalent to [`std::fs::read_to_string`][std::fs::read_to_string], but also performs
|
||||||
|
/// line ending normalization.
|
||||||
|
///
|
||||||
|
/// [std::fs::read_to_string]: https://doc.rust-lang.org/stable/std/fs/fn.read_to_string.html
|
||||||
|
#[inline]
|
||||||
|
pub fn read_to_string_lf_normalized<P: AsRef<Path>>(&self, path: P) -> io::Result<Arc<String>> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let contents = self.inner.lock().unwrap().read_to_string(path)?;
|
||||||
|
|
||||||
|
Ok(contents.replace("\r\n", "\n").into())
|
||||||
|
}
|
||||||
|
|
||||||
/// Write a file to the VFS and the underlying backend.
|
/// Write a file to the VFS and the underlying backend.
|
||||||
///
|
///
|
||||||
/// Roughly equivalent to [`std::fs::write`][std::fs::write].
|
/// Roughly equivalent to [`std::fs::write`][std::fs::write].
|
||||||
@@ -431,3 +473,23 @@ impl VfsLock<'_> {
|
|||||||
self.inner.commit_event(event)
|
self.inner.commit_event(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::{InMemoryFs, Vfs, VfsSnapshot};
|
||||||
|
|
||||||
|
/// https://github.com/rojo-rbx/rojo/issues/899
|
||||||
|
#[test]
|
||||||
|
fn read_to_string_lf_normalized_keeps_trailing_newline() {
|
||||||
|
let mut imfs = InMemoryFs::new();
|
||||||
|
imfs.load_snapshot("test", VfsSnapshot::file("bar\r\nfoo\r\n\r\n"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
vfs.read_to_string_lf_normalized("test").unwrap().as_str(),
|
||||||
|
"bar\nfoo\n\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -74,3 +74,9 @@ impl VfsBackend for NoopBackend {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for NoopBackend {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::io;
|
use std::path::{Path, PathBuf};
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use std::{collections::HashSet, io};
|
||||||
|
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{watcher, DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
@@ -13,6 +13,7 @@ use crate::{DirEntry, Metadata, ReadDir, VfsBackend, VfsEvent};
|
|||||||
pub struct StdBackend {
|
pub struct StdBackend {
|
||||||
watcher: RecommendedWatcher,
|
watcher: RecommendedWatcher,
|
||||||
watcher_receiver: Receiver<VfsEvent>,
|
watcher_receiver: Receiver<VfsEvent>,
|
||||||
|
watches: HashSet<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StdBackend {
|
impl StdBackend {
|
||||||
@@ -48,6 +49,7 @@ impl StdBackend {
|
|||||||
Self {
|
Self {
|
||||||
watcher,
|
watcher,
|
||||||
watcher_receiver: rx,
|
watcher_receiver: rx,
|
||||||
|
watches: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,14 +99,30 @@ impl VfsBackend for StdBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn watch(&mut self, path: &Path) -> io::Result<()> {
|
fn watch(&mut self, path: &Path) -> io::Result<()> {
|
||||||
|
if self.watches.contains(path)
|
||||||
|
|| path
|
||||||
|
.ancestors()
|
||||||
|
.any(|ancestor| self.watches.contains(ancestor))
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
self.watches.insert(path.to_path_buf());
|
||||||
self.watcher
|
self.watcher
|
||||||
.watch(path, RecursiveMode::NonRecursive)
|
.watch(path, RecursiveMode::Recursive)
|
||||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
fn unwatch(&mut self, path: &Path) -> io::Result<()> {
|
||||||
|
self.watches.remove(path);
|
||||||
self.watcher
|
self.watcher
|
||||||
.unwatch(path)
|
.unwatch(path)
|
||||||
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for StdBackend {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ edition = "2018"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = "1.0.99"
|
serde = "1.0.197"
|
||||||
serde_yaml = "0.8.9"
|
serde_yaml = "0.8.26"
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
[tools]
|
|
||||||
rojo = { source = "rojo-rbx/rojo", version = "7.1.1" }
|
|
||||||
run-in-roblox = { source = "rojo-rbx/run-in-roblox", version = "0.3.0" }
|
|
||||||
selene = { source = "Kampfkarren/selene", version = "0.18.2" }
|
|
||||||
1
plugin/Packages/Flipper
Submodule
1
plugin/Packages/Highlighter
Submodule
1
plugin/Packages/Promise
Submodule
1
plugin/Packages/Roact
Submodule
1
plugin/Packages/TestEZ
Submodule
1
plugin/Packages/t
Submodule
1
plugin/Version.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
7.4.0
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
"Plugin": {
|
"Plugin": {
|
||||||
"$path": "src"
|
"$path": "src"
|
||||||
},
|
},
|
||||||
|
"Packages": {
|
||||||
|
"$path": "Packages",
|
||||||
"Log": {
|
"Log": {
|
||||||
"$path": "log"
|
"$path": "log"
|
||||||
},
|
},
|
||||||
@@ -16,18 +18,10 @@
|
|||||||
},
|
},
|
||||||
"RbxDom": {
|
"RbxDom": {
|
||||||
"$path": "rbx_dom_lua"
|
"$path": "rbx_dom_lua"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Roact": {
|
"Version": {
|
||||||
"$path": "modules/roact/src"
|
"$path": "Version.txt"
|
||||||
},
|
|
||||||
"Promise": {
|
|
||||||
"$path": "modules/promise/lib"
|
|
||||||
},
|
|
||||||
"t": {
|
|
||||||
"$path": "modules/t/lib"
|
|
||||||
},
|
|
||||||
"Flipper": {
|
|
||||||
"$path": "modules/flipper/src"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,12 +3,12 @@ Error.__index = Error
|
|||||||
|
|
||||||
Error.Kind = {
|
Error.Kind = {
|
||||||
HttpNotEnabled = {
|
HttpNotEnabled = {
|
||||||
message = "Rojo requires HTTP access, which is not enabled.\n" ..
|
message = "Rojo requires HTTP access, which is not enabled.\n"
|
||||||
"Check your game settings, located in the 'Home' tab of Studio.",
|
.. "Check your game settings, located in the 'Home' tab of Studio.",
|
||||||
},
|
},
|
||||||
ConnectFailed = {
|
ConnectFailed = {
|
||||||
message = "Couldn't connect to the Rojo server.\n" ..
|
message = "Couldn't connect to the Rojo server.\n"
|
||||||
"Make sure the server is running — use 'rojo serve' to run it!",
|
.. "Make sure the server is running — use 'rojo serve' to run it!",
|
||||||
},
|
},
|
||||||
Timeout = {
|
Timeout = {
|
||||||
message = "HTTP request timed out.",
|
message = "HTTP request timed out.",
|
||||||
@@ -63,4 +63,13 @@ function Error.fromRobloxErrorString(message)
|
|||||||
return Error.new(Error.Kind.Unknown, message)
|
return Error.new(Error.Kind.Unknown, message)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Error.fromResponse(response)
|
||||||
|
local lower = (response.body or ""):lower()
|
||||||
|
if response.code == 408 or response.code == 504 or lower:find("timed? ?out") then
|
||||||
|
return Error.new(Error.Kind.Timeout)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Error.new(Error.Kind.Unknown, string.format("%s: %s", tostring(response.code), tostring(response.body)))
|
||||||
|
end
|
||||||
|
|
||||||
return Error
|
return Error
|
||||||
|
|||||||
@@ -30,8 +30,13 @@ local function performRequest(requestParams)
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
if success then
|
if success then
|
||||||
Log.trace("Request {} success, status code {}", requestId, response.StatusCode)
|
Log.trace("Request {} success, response {:#?}", requestId, response)
|
||||||
resolve(HttpResponse.fromRobloxResponse(response))
|
local httpResponse = HttpResponse.fromRobloxResponse(response)
|
||||||
|
if httpResponse:isSuccess() then
|
||||||
|
resolve(httpResponse)
|
||||||
|
else
|
||||||
|
reject(HttpError.fromResponse(httpResponse))
|
||||||
|
end
|
||||||
else
|
else
|
||||||
Log.trace("Request {} failure: {:?}", requestId, response)
|
Log.trace("Request {} failure: {:?}", requestId, response)
|
||||||
reject(HttpError.fromRobloxErrorString(response))
|
reject(HttpError.fromRobloxErrorString(response))
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ types = {
|
|||||||
if ok then
|
if ok then
|
||||||
output[key] = result
|
output[key] = result
|
||||||
else
|
else
|
||||||
local warning = ("Could not decode attribute value of type %q: %s"):format(typeof(value), tostring(result))
|
local warning = ("Could not decode attribute value of type %q: %s"):format(
|
||||||
|
typeof(value),
|
||||||
|
tostring(result)
|
||||||
|
)
|
||||||
warn(warning)
|
warn(warning)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -53,7 +56,10 @@ types = {
|
|||||||
if ok then
|
if ok then
|
||||||
output[key] = result
|
output[key] = result
|
||||||
else
|
else
|
||||||
local warning = ("Could not encode attribute value of type %q: %s"):format(typeof(value), tostring(result))
|
local warning = ("Could not encode attribute value of type %q: %s"):format(
|
||||||
|
typeof(value),
|
||||||
|
tostring(result)
|
||||||
|
)
|
||||||
warn(warning)
|
warn(warning)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -111,6 +117,7 @@ types = {
|
|||||||
local pos = pod.position
|
local pos = pod.position
|
||||||
local orient = pod.orientation
|
local orient = pod.orientation
|
||||||
|
|
||||||
|
--stylua: ignore
|
||||||
return CFrame.new(
|
return CFrame.new(
|
||||||
pos[1], pos[2], pos[3],
|
pos[1], pos[2], pos[3],
|
||||||
orient[1][1], orient[1][2], orient[1][3],
|
orient[1][1], orient[1][2], orient[1][3],
|
||||||
@@ -120,10 +127,7 @@ types = {
|
|||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
local x, y, z,
|
local x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22 = roblox:GetComponents()
|
||||||
r00, r01, r02,
|
|
||||||
r10, r11, r12,
|
|
||||||
r20, r21, r22 = roblox:GetComponents()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
position = { x, y, z },
|
position = { x, y, z },
|
||||||
@@ -161,10 +165,7 @@ types = {
|
|||||||
local keypoints = {}
|
local keypoints = {}
|
||||||
|
|
||||||
for index, keypoint in ipairs(pod.keypoints) do
|
for index, keypoint in ipairs(pod.keypoints) do
|
||||||
keypoints[index] = ColorSequenceKeypoint.new(
|
keypoints[index] = ColorSequenceKeypoint.new(keypoint.time, types.Color3.fromPod(keypoint.color))
|
||||||
keypoint.time,
|
|
||||||
types.Color3.fromPod(keypoint.color)
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return ColorSequence.new(keypoints)
|
return ColorSequence.new(keypoints)
|
||||||
@@ -238,6 +239,23 @@ types = {
|
|||||||
toPod = serializeFloat,
|
toPod = serializeFloat,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Font = {
|
||||||
|
fromPod = function(pod)
|
||||||
|
return Font.new(
|
||||||
|
pod.family,
|
||||||
|
if pod.weight ~= nil then Enum.FontWeight[pod.weight] else nil,
|
||||||
|
if pod.style ~= nil then Enum.FontStyle[pod.style] else nil
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
toPod = function(roblox)
|
||||||
|
return {
|
||||||
|
family = roblox.Family,
|
||||||
|
weight = roblox.Weight.Name,
|
||||||
|
style = roblox.Style.Name,
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
Int32 = {
|
Int32 = {
|
||||||
fromPod = identity,
|
fromPod = identity,
|
||||||
toPod = identity,
|
toPod = identity,
|
||||||
@@ -248,6 +266,27 @@ 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
|
||||||
|
return pod
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
|
||||||
NumberRange = {
|
NumberRange = {
|
||||||
fromPod = unpackDecoder(NumberRange.new),
|
fromPod = unpackDecoder(NumberRange.new),
|
||||||
|
|
||||||
@@ -261,11 +300,7 @@ types = {
|
|||||||
local keypoints = {}
|
local keypoints = {}
|
||||||
|
|
||||||
for index, keypoint in ipairs(pod.keypoints) do
|
for index, keypoint in ipairs(pod.keypoints) do
|
||||||
keypoints[index] = NumberSequenceKeypoint.new(
|
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, keypoint.value, keypoint.envelope)
|
||||||
keypoint.time,
|
|
||||||
keypoint.value,
|
|
||||||
keypoint.envelope
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return NumberSequence.new(keypoints)
|
return NumberSequence.new(keypoints)
|
||||||
@@ -320,10 +355,7 @@ types = {
|
|||||||
|
|
||||||
Ray = {
|
Ray = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
return Ray.new(
|
return Ray.new(types.Vector3.fromPod(pod.origin), types.Vector3.fromPod(pod.direction))
|
||||||
types.Vector3.fromPod(pod.origin),
|
|
||||||
types.Vector3.fromPod(pod.direction)
|
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
@@ -336,10 +368,7 @@ types = {
|
|||||||
|
|
||||||
Rect = {
|
Rect = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
return Rect.new(
|
return Rect.new(types.Vector2.fromPod(pod[1]), types.Vector2.fromPod(pod[2]))
|
||||||
types.Vector2.fromPod(pod[1]),
|
|
||||||
types.Vector2.fromPod(pod[2])
|
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
@@ -351,31 +380,28 @@ types = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
Ref = {
|
Ref = {
|
||||||
fromPod = function(_pod)
|
fromPod = function(_)
|
||||||
error("Ref cannot be decoded on its own")
|
error("Ref cannot be decoded on its own")
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(_roblox)
|
toPod = function(_)
|
||||||
error("Ref can not be encoded on its own")
|
error("Ref can not be encoded on its own")
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
Region3 = {
|
Region3 = {
|
||||||
fromPod = function(pod)
|
fromPod = function(_)
|
||||||
error("Region3 is not implemented")
|
error("Region3 is not implemented")
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(_)
|
||||||
error("Region3 is not implemented")
|
error("Region3 is not implemented")
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
|
|
||||||
Region3int16 = {
|
Region3int16 = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
return Region3int16.new(
|
return Region3int16.new(types.Vector3int16.fromPod(pod[1]), types.Vector3int16.fromPod(pod[2]))
|
||||||
types.Vector3int16.fromPod(pod[1]),
|
|
||||||
types.Vector3int16.fromPod(pod[2])
|
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
@@ -387,11 +413,11 @@ types = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
SharedString = {
|
SharedString = {
|
||||||
fromPod = function(pod)
|
fromPod = function(_pod)
|
||||||
error("SharedString is not supported")
|
error("SharedString is not supported")
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(_roblox)
|
||||||
error("SharedString is not supported")
|
error("SharedString is not supported")
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
@@ -411,10 +437,7 @@ types = {
|
|||||||
|
|
||||||
UDim2 = {
|
UDim2 = {
|
||||||
fromPod = function(pod)
|
fromPod = function(pod)
|
||||||
return UDim2.new(
|
return UDim2.new(types.UDim.fromPod(pod[1]), types.UDim.fromPod(pod[2]))
|
||||||
types.UDim.fromPod(pod[1]),
|
|
||||||
types.UDim.fromPod(pod[2])
|
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
toPod = function(roblox)
|
toPod = function(roblox)
|
||||||
@@ -470,9 +493,32 @@ types = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
types.OptionalCFrame = {
|
||||||
|
fromPod = function(pod)
|
||||||
|
if pod == nil then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
return types.CFrame.fromPod(pod)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
toPod = function(roblox)
|
||||||
|
if roblox == nil then
|
||||||
|
return nil
|
||||||
|
else
|
||||||
|
return types.CFrame.toPod(roblox)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
function EncodedValue.decode(encodedValue)
|
function EncodedValue.decode(encodedValue)
|
||||||
local ty, value = next(encodedValue)
|
local ty, value = next(encodedValue)
|
||||||
|
|
||||||
|
if ty == nil then
|
||||||
|
-- If the encoded pair is empty, assume it is an unoccupied optional value
|
||||||
|
return true, nil
|
||||||
|
end
|
||||||
|
|
||||||
local typeImpl = types[ty]
|
local typeImpl = types[ty]
|
||||||
if typeImpl == nil then
|
if typeImpl == nil then
|
||||||
return false, "Couldn't decode value " .. tostring(ty)
|
return false, "Couldn't decode value " .. tostring(ty)
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
return function()
|
|
||||||
local HttpService = game:GetService("HttpService")
|
|
||||||
|
|
||||||
local EncodedValue = require(script.Parent.EncodedValue)
|
|
||||||
local allValues = require(script.Parent.allValues)
|
|
||||||
|
|
||||||
local function deepEq(a, b)
|
|
||||||
if typeof(a) ~= typeof(b) then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local ty = typeof(a)
|
|
||||||
|
|
||||||
if ty == "table" then
|
|
||||||
local visited = {}
|
|
||||||
|
|
||||||
for key, valueA in pairs(a) do
|
|
||||||
visited[key] = true
|
|
||||||
|
|
||||||
if not deepEq(valueA, b[key]) then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for key, valueB in pairs(b) do
|
|
||||||
if visited[key] then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
if not deepEq(valueB, a[key]) then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
return a == b
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local extraAssertions = {
|
|
||||||
CFrame = function(value)
|
|
||||||
expect(value).to.equal(CFrame.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12))
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
for testName, testEntry in pairs(allValues) do
|
|
||||||
it("round trip " .. testName, function()
|
|
||||||
local ok, decoded = EncodedValue.decode(testEntry.value)
|
|
||||||
assert(ok, decoded)
|
|
||||||
|
|
||||||
if extraAssertions[testName] ~= nil then
|
|
||||||
extraAssertions[testName](decoded)
|
|
||||||
end
|
|
||||||
|
|
||||||
local ok, encoded = EncodedValue.encode(decoded, testEntry.ty)
|
|
||||||
assert(ok, encoded)
|
|
||||||
|
|
||||||
if not deepEq(encoded, testEntry.value) then
|
|
||||||
local expected = HttpService:JSONEncode(testEntry.value)
|
|
||||||
local actual = HttpService:JSONEncode(encoded)
|
|
||||||
|
|
||||||
local message = string.format(
|
|
||||||
"Round-trip results did not match.\nExpected:\n%s\nActual:\n%s",
|
|
||||||
expected, actual
|
|
||||||
)
|
|
||||||
|
|
||||||
error(message)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -53,6 +53,11 @@ function PropertyDescriptor:read(instance)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if self.scriptability == "Custom" then
|
if self.scriptability == "Custom" then
|
||||||
|
if customProperties[self.className] == nil then
|
||||||
|
local fullName = ("%s.%s"):format(instance.className, self.name)
|
||||||
|
return false, Error.new(Error.Kind.PropertyNotReadable, fullName)
|
||||||
|
end
|
||||||
|
|
||||||
local interface = customProperties[self.className][self.name]
|
local interface = customProperties[self.className][self.name]
|
||||||
|
|
||||||
return interface.read(instance, self.name)
|
return interface.read(instance, self.name)
|
||||||
@@ -79,6 +84,11 @@ function PropertyDescriptor:write(instance, value)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if self.scriptability == "Custom" then
|
if self.scriptability == "Custom" then
|
||||||
|
if customProperties[self.className] == nil then
|
||||||
|
local fullName = ("%s.%s"):format(instance.className, self.name)
|
||||||
|
return false, Error.new(Error.Kind.PropertyNotWritable, fullName)
|
||||||
|
end
|
||||||
|
|
||||||
local interface = customProperties[self.className][self.name]
|
local interface = customProperties[self.className][self.name]
|
||||||
|
|
||||||
return interface.write(instance, self.name, value)
|
return interface.write(instance, self.name, value)
|
||||||
|
|||||||
@@ -207,6 +207,17 @@
|
|||||||
},
|
},
|
||||||
"ty": "Float64"
|
"ty": "Float64"
|
||||||
},
|
},
|
||||||
|
"Font": {
|
||||||
|
"value": {
|
||||||
|
"Font": {
|
||||||
|
"family": "rbxasset://fonts/families/SourceSansPro.json",
|
||||||
|
"weight": "Regular",
|
||||||
|
"style": "Normal",
|
||||||
|
"cachedFaceId": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ty": "Font"
|
||||||
|
},
|
||||||
"Int32": {
|
"Int32": {
|
||||||
"value": {
|
"value": {
|
||||||
"Int32": 6014
|
"Int32": 6014
|
||||||
@@ -219,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": [
|
||||||
@@ -247,6 +370,41 @@
|
|||||||
},
|
},
|
||||||
"ty": "NumberSequence"
|
"ty": "NumberSequence"
|
||||||
},
|
},
|
||||||
|
"OptionalCFrame-None": {
|
||||||
|
"value": {
|
||||||
|
"OptionalCFrame": null
|
||||||
|
},
|
||||||
|
"ty": "OptionalCFrame"
|
||||||
|
},
|
||||||
|
"OptionalCFrame-Some": {
|
||||||
|
"value": {
|
||||||
|
"OptionalCFrame": {
|
||||||
|
"position": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"orientation": [
|
||||||
|
[
|
||||||
|
1.0,
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ty": "OptionalCFrame"
|
||||||
|
},
|
||||||
"PhysicalProperties-Custom": {
|
"PhysicalProperties-Custom": {
|
||||||
"value": {
|
"value": {
|
||||||
"PhysicalProperties": {
|
"PhysicalProperties": {
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
return function()
|
|
||||||
local base64 = require(script.Parent.base64)
|
|
||||||
|
|
||||||
it("should encode and decode", function()
|
|
||||||
local function try(str, expected)
|
|
||||||
local encoded = base64.encode(str)
|
|
||||||
expect(encoded).to.equal(expected)
|
|
||||||
expect(base64.decode(encoded)).to.equal(str)
|
|
||||||
end
|
|
||||||
|
|
||||||
try("Man", "TWFu")
|
|
||||||
try("Ma", "TWE=")
|
|
||||||
try("M", "TQ==")
|
|
||||||
try("ManM", "TWFuTQ==")
|
|
||||||
try(
|
|
||||||
[[Man is distinguished, not only by his reason, but by this ]]..
|
|
||||||
[[singular passion from other animals, which is a lust of the ]]..
|
|
||||||
[[mind, that by a perseverance of delight in the continued and ]]..
|
|
||||||
[[indefatigable generation of knowledge, exceeds the short ]]..
|
|
||||||
[[vehemence of any carnal pleasure.]],
|
|
||||||
[[TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sI]]..
|
|
||||||
[[GJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYW]]..
|
|
||||||
[[xzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJ]]..
|
|
||||||
[[zZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRl]]..
|
|
||||||
[[ZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZ]]..
|
|
||||||
[[SBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=]]
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
@@ -1,4 +1,45 @@
|
|||||||
local CollectionService = game:GetService("CollectionService")
|
local CollectionService = game:GetService("CollectionService")
|
||||||
|
local ScriptEditorService = game:GetService("ScriptEditorService")
|
||||||
|
|
||||||
|
--- 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function isAttributeNameValid(attributeName)
|
||||||
|
-- For SetAttribute to succeed, the attribute name must be less than or
|
||||||
|
-- equal to 100 characters...
|
||||||
|
return #attributeName <= 100
|
||||||
|
-- ...and must only contain alphanumeric characters, periods, hyphens,
|
||||||
|
-- underscores, or forward slashes.
|
||||||
|
and attributeName:match("[^%w%.%-_/]") == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isAttributeNameReserved(attributeName)
|
||||||
|
-- For SetAttribute to succeed, attribute names must not use the RBX
|
||||||
|
-- prefix, which is reserved by Roblox.
|
||||||
|
return attributeName:sub(1, 3) == "RBX"
|
||||||
|
end
|
||||||
|
|
||||||
-- Defines how to read and write properties that aren't directly scriptable.
|
-- Defines how to read and write properties that aren't directly scriptable.
|
||||||
--
|
--
|
||||||
@@ -11,18 +52,40 @@ return {
|
|||||||
end,
|
end,
|
||||||
write = function(instance, _, value)
|
write = function(instance, _, value)
|
||||||
local existing = instance:GetAttributes()
|
local existing = instance:GetAttributes()
|
||||||
|
local didAllWritesSucceed = true
|
||||||
|
|
||||||
for key, attr in pairs(value) do
|
for attributeName, attributeValue in pairs(value) do
|
||||||
instance:SetAttribute(key, attr)
|
if isAttributeNameReserved(attributeName) then
|
||||||
|
-- If the attribute name is reserved, then we don't
|
||||||
|
-- really care about reporting any failures about
|
||||||
|
-- it.
|
||||||
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
for key in pairs(existing) do
|
if not isAttributeNameValid(attributeName) then
|
||||||
if value[key] == nil then
|
didAllWritesSucceed = false
|
||||||
instance:SetAttribute(key, nil)
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
instance:SetAttribute(attributeName, attributeValue)
|
||||||
|
end
|
||||||
|
|
||||||
|
for existingAttributeName in pairs(existing) do
|
||||||
|
if isAttributeNameReserved(existingAttributeName) then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
if not isAttributeNameValid(existingAttributeName) then
|
||||||
|
didAllWritesSucceed = false
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
if value[existingAttributeName] == nil then
|
||||||
|
instance:SetAttribute(existingAttributeName, nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
return didAllWritesSucceed
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
Tags = {
|
Tags = {
|
||||||
@@ -52,13 +115,86 @@ return {
|
|||||||
},
|
},
|
||||||
LocalizationTable = {
|
LocalizationTable = {
|
||||||
Contents = {
|
Contents = {
|
||||||
read = function(instance, key)
|
read = function(instance, _)
|
||||||
return true, instance:GetContents()
|
return true, instance:GetContents()
|
||||||
end,
|
end,
|
||||||
write = function(instance, key, value)
|
write = function(instance, _, value)
|
||||||
instance:SetContents(value)
|
instance:SetContents(value)
|
||||||
return true
|
return true
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Model = {
|
||||||
|
Scale = {
|
||||||
|
read = function(instance, _, _)
|
||||||
|
return true, instance:GetScale()
|
||||||
|
end,
|
||||||
|
write = function(instance, _, value)
|
||||||
|
return true, instance:ScaleTo(value)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
WorldPivotData = {
|
||||||
|
read = function(instance)
|
||||||
|
return true, instance.WorldPivot
|
||||||
|
end,
|
||||||
|
write = function(instance, _, value)
|
||||||
|
if value == nil then
|
||||||
|
return true, nil
|
||||||
|
else
|
||||||
|
instance.WorldPivot = value
|
||||||
|
return true
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Script = {
|
||||||
|
Source = {
|
||||||
|
read = function(instance: Script)
|
||||||
|
return true, ScriptEditorService:GetEditorSource(instance)
|
||||||
|
end,
|
||||||
|
write = function(instance: Script, _, value: string)
|
||||||
|
task.spawn(function()
|
||||||
|
ScriptEditorService:UpdateSourceAsync(instance, function()
|
||||||
|
return value
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ModuleScript = {
|
||||||
|
Source = {
|
||||||
|
read = function(instance: ModuleScript)
|
||||||
|
return true, ScriptEditorService:GetEditorSource(instance)
|
||||||
|
end,
|
||||||
|
write = function(instance: ModuleScript, _, value: string)
|
||||||
|
task.spawn(function()
|
||||||
|
ScriptEditorService:UpdateSourceAsync(instance, function()
|
||||||
|
return value
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ local function findCanonicalPropertyDescriptor(className, propertyName)
|
|||||||
return PropertyDescriptor.fromRaw(
|
return PropertyDescriptor.fromRaw(
|
||||||
currentClass.Properties[aliasData.AliasFor],
|
currentClass.Properties[aliasData.AliasFor],
|
||||||
currentClassName,
|
currentClassName,
|
||||||
aliasData.AliasFor)
|
aliasData.AliasFor
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
return function()
|
|
||||||
local RbxDom = require(script.Parent)
|
|
||||||
|
|
||||||
it("should load", function()
|
|
||||||
expect(RbxDom).to.be.ok()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
@@ -1,19 +1,11 @@
|
|||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
|
||||||
local TestEZ = require(ReplicatedStorage.TestEZ)
|
local TestEZ = require(ReplicatedStorage.Packages.TestEZ)
|
||||||
|
|
||||||
local Rojo = ReplicatedStorage.Rojo
|
local Rojo = ReplicatedStorage.Rojo
|
||||||
|
|
||||||
local DevSettings = require(Rojo.Plugin.DevSettings)
|
local Settings = require(Rojo.Plugin.Settings)
|
||||||
|
Settings:set("logLevel", "Trace")
|
||||||
local setDevSettings = not DevSettings:hasChangedValues()
|
Settings:set("typecheckingEnabled", true)
|
||||||
|
|
||||||
if setDevSettings then
|
|
||||||
DevSettings:createTestSettings()
|
|
||||||
end
|
|
||||||
|
|
||||||
require(Rojo.Plugin.runTests)(TestEZ)
|
require(Rojo.Plugin.runTests)(TestEZ)
|
||||||
|
|
||||||
if setDevSettings then
|
|
||||||
DevSettings:resetValues()
|
|
||||||
end
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
local Http = require(script.Parent.Parent.Http)
|
local Packages = script.Parent.Parent.Packages
|
||||||
local Log = require(script.Parent.Parent.Log)
|
local Http = require(Packages.Http)
|
||||||
local Promise = require(script.Parent.Parent.Promise)
|
local Log = require(Packages.Log)
|
||||||
|
local Promise = require(Packages.Promise)
|
||||||
|
|
||||||
local Config = require(script.Parent.Config)
|
local Config = require(script.Parent.Config)
|
||||||
local Types = require(script.Parent.Types)
|
local Types = require(script.Parent.Types)
|
||||||
@@ -10,13 +11,6 @@ local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
|
|||||||
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
|
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
|
||||||
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
|
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
|
||||||
|
|
||||||
--[[
|
|
||||||
Returns a promise that will never resolve nor reject.
|
|
||||||
]]
|
|
||||||
local function hangingPromise()
|
|
||||||
return Promise.new(function() end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function rejectFailedRequests(response)
|
local function rejectFailedRequests(response)
|
||||||
if response.code >= 400 then
|
if response.code >= 400 then
|
||||||
local message = string.format("HTTP %s:\n%s", tostring(response.code), response.body)
|
local message = string.format("HTTP %s:\n%s", tostring(response.code), response.body)
|
||||||
@@ -30,15 +24,17 @@ end
|
|||||||
local function rejectWrongProtocolVersion(infoResponseBody)
|
local function rejectWrongProtocolVersion(infoResponseBody)
|
||||||
if infoResponseBody.protocolVersion ~= Config.protocolVersion then
|
if infoResponseBody.protocolVersion ~= Config.protocolVersion then
|
||||||
local message = (
|
local message = (
|
||||||
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." ..
|
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible."
|
||||||
"\nMake sure you have matching versions of both the Rojo plugin and server!" ..
|
.. "\nMake sure you have matching versions of both the Rojo plugin and server!"
|
||||||
"\n\nYour client is version %s, with protocol version %s. It expects server version %s." ..
|
.. "\n\nYour client is version %s, with protocol version %s. It expects server version %s."
|
||||||
"\nYour server is version %s, with protocol version %s." ..
|
.. "\nYour server is version %s, with protocol version %s."
|
||||||
"\n\nGo to https://github.com/rojo-rbx/rojo for more details."
|
.. "\n\nGo to https://github.com/rojo-rbx/rojo for more details."
|
||||||
):format(
|
):format(
|
||||||
Version.display(Config.version), Config.protocolVersion,
|
Version.display(Config.version),
|
||||||
|
Config.protocolVersion,
|
||||||
Config.expectedServerVersionString,
|
Config.expectedServerVersionString,
|
||||||
infoResponseBody.serverVersion, infoResponseBody.protocolVersion
|
infoResponseBody.serverVersion,
|
||||||
|
infoResponseBody.protocolVersion
|
||||||
)
|
)
|
||||||
|
|
||||||
return Promise.reject(message)
|
return Promise.reject(message)
|
||||||
@@ -65,14 +61,11 @@ local function rejectWrongPlaceId(infoResponseBody)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local message = (
|
local message = (
|
||||||
"Found a Rojo server, but its project is set to only be used with a specific list of places." ..
|
"Found a Rojo server, but its project is set to only be used with a specific list of places."
|
||||||
"\nYour place ID is %s, but needs to be one of these:" ..
|
.. "\nYour place ID is %s, but needs to be one of these:"
|
||||||
"\n%s" ..
|
.. "\n%s"
|
||||||
"\n\nTo change this list, edit 'servePlaceIds' in your .project.json file."
|
.. "\n\nTo change this list, edit 'servePlaceIds' in your .project.json file."
|
||||||
):format(
|
):format(tostring(game.PlaceId), table.concat(idList, "\n"))
|
||||||
tostring(game.PlaceId),
|
|
||||||
table.concat(idList, "\n")
|
|
||||||
)
|
|
||||||
|
|
||||||
return Promise.reject(message)
|
return Promise.reject(message)
|
||||||
end
|
end
|
||||||
@@ -85,13 +78,14 @@ local ApiContext = {}
|
|||||||
ApiContext.__index = ApiContext
|
ApiContext.__index = ApiContext
|
||||||
|
|
||||||
function ApiContext.new(baseUrl)
|
function ApiContext.new(baseUrl)
|
||||||
assert(type(baseUrl) == "string")
|
assert(type(baseUrl) == "string", "baseUrl must be a string")
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
__baseUrl = baseUrl,
|
__baseUrl = baseUrl,
|
||||||
__sessionId = nil,
|
__sessionId = nil,
|
||||||
__messageCursor = -1,
|
__messageCursor = -1,
|
||||||
__connected = true,
|
__connected = true,
|
||||||
|
__activeRequests = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
return setmetatable(self, ApiContext)
|
return setmetatable(self, ApiContext)
|
||||||
@@ -112,6 +106,11 @@ end
|
|||||||
|
|
||||||
function ApiContext:disconnect()
|
function ApiContext:disconnect()
|
||||||
self.__connected = false
|
self.__connected = false
|
||||||
|
for request in self.__activeRequests do
|
||||||
|
Log.trace("Cancelling request {}", request)
|
||||||
|
request:cancel()
|
||||||
|
end
|
||||||
|
self.__activeRequests = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:setMessageCursor(index)
|
function ApiContext:setMessageCursor(index)
|
||||||
@@ -141,10 +140,7 @@ end
|
|||||||
function ApiContext:read(ids)
|
function ApiContext:read(ids)
|
||||||
local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||||
|
|
||||||
return Http.get(url)
|
return Http.get(url):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||||
:andThen(rejectFailedRequests)
|
|
||||||
:andThen(Http.Response.json)
|
|
||||||
:andThen(function(body)
|
|
||||||
if body.sessionId ~= self.__sessionId then
|
if body.sessionId ~= self.__sessionId then
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
@@ -189,13 +185,10 @@ function ApiContext:write(patch)
|
|||||||
|
|
||||||
body = Http.jsonEncode(body)
|
body = Http.jsonEncode(body)
|
||||||
|
|
||||||
return Http.post(url, body)
|
return Http.post(url, body):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(responseBody)
|
||||||
:andThen(rejectFailedRequests)
|
Log.info("Write response: {:?}", responseBody)
|
||||||
:andThen(Http.Response.json)
|
|
||||||
:andThen(function(body)
|
|
||||||
Log.info("Write response: {:?}", body)
|
|
||||||
|
|
||||||
return body
|
return responseBody
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -203,24 +196,25 @@ function ApiContext:retrieveMessages()
|
|||||||
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
|
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
|
||||||
|
|
||||||
local function sendRequest()
|
local function sendRequest()
|
||||||
return Http.get(url)
|
local request = Http.get(url):catch(function(err)
|
||||||
:catch(function(err)
|
if err.type == Http.Error.Kind.Timeout and self.__connected then
|
||||||
if err.type == Http.Error.Kind.Timeout then
|
|
||||||
if self.__connected then
|
|
||||||
return sendRequest()
|
return sendRequest()
|
||||||
else
|
|
||||||
return hangingPromise()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
Log.trace("Tracking request {}", request)
|
||||||
|
self.__activeRequests[request] = true
|
||||||
|
|
||||||
|
return request:finally(function(...)
|
||||||
|
Log.trace("Cleaning up request {}", request)
|
||||||
|
self.__activeRequests[request] = nil
|
||||||
|
return ...
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return sendRequest()
|
return sendRequest():andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||||
:andThen(rejectFailedRequests)
|
|
||||||
:andThen(Http.Response.json)
|
|
||||||
:andThen(function(body)
|
|
||||||
if body.sessionId ~= self.__sessionId then
|
if body.sessionId ~= self.__sessionId then
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
@@ -236,10 +230,7 @@ end
|
|||||||
function ApiContext:open(id)
|
function ApiContext:open(id)
|
||||||
local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
|
local url = ("%s/api/open/%s"):format(self.__baseUrl, id)
|
||||||
|
|
||||||
return Http.post(url, "")
|
return Http.post(url, ""):andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
|
||||||
:andThen(rejectFailedRequests)
|
|
||||||
:andThen(Http.Response.json)
|
|
||||||
:andThen(function(body)
|
|
||||||
if body.sessionId ~= self.__sessionId then
|
if body.sessionId ~= self.__sessionId then
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
@@ -23,8 +24,10 @@ local function BorderedContainer(props)
|
|||||||
layoutOrder = props.layoutOrder,
|
layoutOrder = props.layoutOrder,
|
||||||
}, {
|
}, {
|
||||||
Content = e("Frame", {
|
Content = e("Frame", {
|
||||||
Size = UDim2.new(1, 0, 1, 0),
|
Size = UDim2.new(1, -2, 1, -2),
|
||||||
|
Position = UDim2.new(0, 1, 0, 1),
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
ZIndex = 2,
|
||||||
}, props[Roact.Children]),
|
}, props[Roact.Children]),
|
||||||
|
|
||||||
Border = e(SlicedImage, {
|
Border = e(SlicedImage, {
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
local Flipper = require(Rojo.Flipper)
|
local Flipper = require(Packages.Flipper)
|
||||||
|
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||||
|
|
||||||
local SlicedImage = require(script.Parent.SlicedImage)
|
local SlicedImage = require(script.Parent.SlicedImage)
|
||||||
|
local Tooltip = require(script.Parent.Tooltip)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -21,12 +23,10 @@ end
|
|||||||
|
|
||||||
function Checkbox:didUpdate(lastProps)
|
function Checkbox:didUpdate(lastProps)
|
||||||
if lastProps.active ~= self.props.active then
|
if lastProps.active ~= self.props.active then
|
||||||
self.motor:setGoal(
|
self.motor:setGoal(Flipper.Spring.new(self.props.active and 1 or 0, {
|
||||||
Flipper.Spring.new(self.props.active and 1 or 0, {
|
|
||||||
frequency = 6,
|
frequency = 6,
|
||||||
dampingRatio = 1.1,
|
dampingRatio = 1.1,
|
||||||
})
|
}))
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -49,8 +49,18 @@ function Checkbox:render()
|
|||||||
ZIndex = self.props.zIndex,
|
ZIndex = self.props.zIndex,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
[Roact.Event.Activated] = self.props.onClick,
|
[Roact.Event.Activated] = function()
|
||||||
|
if self.props.locked then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.props.onClick()
|
||||||
|
end,
|
||||||
}, {
|
}, {
|
||||||
|
StateTip = e(Tooltip.Trigger, {
|
||||||
|
text = (if self.props.locked then "[LOCKED] " else "")
|
||||||
|
.. (if self.props.active then "Enabled" else "Disabled"),
|
||||||
|
}),
|
||||||
|
|
||||||
Active = e(SlicedImage, {
|
Active = e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = theme.Active.BackgroundColor,
|
color = theme.Active.BackgroundColor,
|
||||||
@@ -59,7 +69,7 @@ function Checkbox:render()
|
|||||||
zIndex = 2,
|
zIndex = 2,
|
||||||
}, {
|
}, {
|
||||||
Icon = e("ImageLabel", {
|
Icon = e("ImageLabel", {
|
||||||
Image = Assets.Images.Checkbox.Active,
|
Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Active,
|
||||||
ImageColor3 = theme.Active.IconColor,
|
ImageColor3 = theme.Active.IconColor,
|
||||||
ImageTransparency = activeTransparency,
|
ImageTransparency = activeTransparency,
|
||||||
|
|
||||||
@@ -78,7 +88,9 @@ function Checkbox:render()
|
|||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
}, {
|
}, {
|
||||||
Icon = e("ImageLabel", {
|
Icon = e("ImageLabel", {
|
||||||
Image = Assets.Images.Checkbox.Inactive,
|
Image = if self.props.locked
|
||||||
|
then Assets.Images.Checkbox.Locked
|
||||||
|
else Assets.Images.Checkbox.Inactive,
|
||||||
ImageColor3 = theme.Inactive.IconColor,
|
ImageColor3 = theme.Inactive.IconColor,
|
||||||
ImageTransparency = self.props.transparency,
|
ImageTransparency = self.props.transparency,
|
||||||
|
|
||||||
|
|||||||
126
plugin/src/App/Components/ClassIcon.lua
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
local StudioService = game:GetService("StudioService")
|
||||||
|
local AssetService = game:GetService("AssetService")
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local EditableImage = require(Plugin.App.Components.EditableImage)
|
||||||
|
|
||||||
|
local imageCache = {}
|
||||||
|
local function getImageSizeAndPixels(image)
|
||||||
|
if not imageCache[image] then
|
||||||
|
local editableImage = AssetService:CreateEditableImageAsync(image)
|
||||||
|
imageCache[image] = {
|
||||||
|
Size = editableImage.Size,
|
||||||
|
Pixels = editableImage:ReadPixels(Vector2.zero, editableImage.Size),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return imageCache[image].Size, table.clone(imageCache[image].Pixels)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getRecoloredClassIcon(className, color)
|
||||||
|
local iconProps = StudioService:GetClassIcon(className)
|
||||||
|
|
||||||
|
if iconProps and color then
|
||||||
|
local success, editableImageSize, editableImagePixels = pcall(function()
|
||||||
|
local size, pixels = getImageSizeAndPixels(iconProps.Image)
|
||||||
|
|
||||||
|
local minVal, maxVal = math.huge, -math.huge
|
||||||
|
for i = 1, #pixels, 4 do
|
||||||
|
if pixels[i + 3] == 0 then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
|
||||||
|
|
||||||
|
minVal = math.min(minVal, pixelVal)
|
||||||
|
maxVal = math.max(maxVal, pixelVal)
|
||||||
|
end
|
||||||
|
|
||||||
|
local hue, sat, val = color:ToHSV()
|
||||||
|
for i = 1, #pixels, 4 do
|
||||||
|
if pixels[i + 3] == 0 then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
local pixelVal = math.max(pixels[i], pixels[i + 1], pixels[i + 2])
|
||||||
|
local newVal = val
|
||||||
|
if minVal < maxVal then
|
||||||
|
-- Remap minVal - maxVal to val*0.9 - val
|
||||||
|
newVal = val * (0.9 + 0.1 * (pixelVal - minVal) / (maxVal - minVal))
|
||||||
|
end
|
||||||
|
|
||||||
|
local newPixelColor = Color3.fromHSV(hue, sat, newVal)
|
||||||
|
pixels[i], pixels[i + 1], pixels[i + 2] = newPixelColor.R, newPixelColor.G, newPixelColor.B
|
||||||
|
end
|
||||||
|
return size, pixels
|
||||||
|
end)
|
||||||
|
if success then
|
||||||
|
iconProps.EditableImagePixels = editableImagePixels
|
||||||
|
iconProps.EditableImageSize = editableImageSize
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return iconProps
|
||||||
|
end
|
||||||
|
|
||||||
|
local ClassIcon = Roact.PureComponent:extend("ClassIcon")
|
||||||
|
|
||||||
|
function ClassIcon:init()
|
||||||
|
self.state = {
|
||||||
|
iconProps = nil,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClassIcon:updateIcon()
|
||||||
|
local props = self.props
|
||||||
|
local iconProps = getRecoloredClassIcon(props.className, props.color)
|
||||||
|
self:setState({
|
||||||
|
iconProps = iconProps,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClassIcon:didMount()
|
||||||
|
self:updateIcon()
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClassIcon:didUpdate(lastProps)
|
||||||
|
if lastProps.className ~= self.props.className or lastProps.color ~= self.props.color then
|
||||||
|
self:updateIcon()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ClassIcon:render()
|
||||||
|
local iconProps = self.state.iconProps
|
||||||
|
if not iconProps then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return e(
|
||||||
|
"ImageLabel",
|
||||||
|
{
|
||||||
|
Size = self.props.size,
|
||||||
|
Position = self.props.position,
|
||||||
|
LayoutOrder = self.props.layoutOrder,
|
||||||
|
AnchorPoint = self.props.anchorPoint,
|
||||||
|
ImageTransparency = self.props.transparency,
|
||||||
|
Image = iconProps.Image,
|
||||||
|
ImageRectOffset = iconProps.ImageRectOffset,
|
||||||
|
ImageRectSize = iconProps.ImageRectSize,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
},
|
||||||
|
if iconProps.EditableImagePixels
|
||||||
|
then e(EditableImage, {
|
||||||
|
size = iconProps.EditableImageSize,
|
||||||
|
pixels = iconProps.EditableImagePixels,
|
||||||
|
})
|
||||||
|
else nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ClassIcon
|
||||||
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
|
||||||
176
plugin/src/App/Components/Dropdown.lua
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
local TextService = game:GetService("TextService")
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
local Flipper = require(Packages.Flipper)
|
||||||
|
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||||
|
|
||||||
|
local SlicedImage = require(script.Parent.SlicedImage)
|
||||||
|
local ScrollingFrame = require(script.Parent.ScrollingFrame)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local Dropdown = Roact.Component:extend("Dropdown")
|
||||||
|
|
||||||
|
function Dropdown:init()
|
||||||
|
self.openMotor = Flipper.SingleMotor.new(0)
|
||||||
|
self.openBinding = bindingUtil.fromMotor(self.openMotor)
|
||||||
|
|
||||||
|
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
open = false,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function Dropdown:didUpdate(prevProps)
|
||||||
|
if self.props.locked and not prevProps.locked then
|
||||||
|
self:setState({
|
||||||
|
open = false,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
self.openMotor:setGoal(Flipper.Spring.new(self.state.open and 1 or 0, {
|
||||||
|
frequency = 6,
|
||||||
|
dampingRatio = 1.1,
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
function Dropdown:render()
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
theme = theme.Dropdown
|
||||||
|
|
||||||
|
local optionButtons = {}
|
||||||
|
local width = -1
|
||||||
|
for i, option in self.props.options do
|
||||||
|
local text = tostring(option or "")
|
||||||
|
local textSize = TextService:GetTextSize(text, 15, Enum.Font.GothamMedium, Vector2.new(math.huge, 20))
|
||||||
|
if textSize.X > width then
|
||||||
|
width = textSize.X
|
||||||
|
end
|
||||||
|
|
||||||
|
optionButtons[text] = e("TextButton", {
|
||||||
|
Text = text,
|
||||||
|
LayoutOrder = i,
|
||||||
|
Size = UDim2.new(1, 0, 0, 24),
|
||||||
|
BackgroundColor3 = theme.BackgroundColor,
|
||||||
|
TextTransparency = self.props.transparency,
|
||||||
|
BackgroundTransparency = self.props.transparency,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
TextColor3 = theme.TextColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextSize = 15,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
|
||||||
|
[Roact.Event.Activated] = function()
|
||||||
|
if self.props.locked then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self:setState({
|
||||||
|
open = false,
|
||||||
|
})
|
||||||
|
self.props.onClick(option)
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
Padding = e("UIPadding", {
|
||||||
|
PaddingLeft = UDim.new(0, 6),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return e("ImageButton", {
|
||||||
|
Size = UDim2.new(0, width + 50, 0, 28),
|
||||||
|
Position = self.props.position,
|
||||||
|
AnchorPoint = self.props.anchorPoint,
|
||||||
|
LayoutOrder = self.props.layoutOrder,
|
||||||
|
ZIndex = self.props.zIndex,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
|
[Roact.Event.Activated] = function()
|
||||||
|
if self.props.locked then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self:setState({
|
||||||
|
open = not self.state.open,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
Border = e(SlicedImage, {
|
||||||
|
slice = Assets.Slices.RoundedBorder,
|
||||||
|
color = theme.BorderColor,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
}, {
|
||||||
|
DropArrow = e("ImageLabel", {
|
||||||
|
Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow,
|
||||||
|
ImageColor3 = theme.IconColor,
|
||||||
|
ImageTransparency = self.props.transparency,
|
||||||
|
|
||||||
|
Size = UDim2.new(0, 18, 0, 18),
|
||||||
|
Position = UDim2.new(1, -6, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(1, 0.5),
|
||||||
|
Rotation = self.openBinding:map(function(a)
|
||||||
|
return a * 180
|
||||||
|
end),
|
||||||
|
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}),
|
||||||
|
Active = e("TextLabel", {
|
||||||
|
Size = UDim2.new(1, -30, 1, 0),
|
||||||
|
Position = UDim2.new(0, 6, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = self.props.active,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 15,
|
||||||
|
TextColor3 = theme.TextColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = self.props.transparency,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Options = if self.state.open
|
||||||
|
then e(SlicedImage, {
|
||||||
|
slice = Assets.Slices.RoundedBackground,
|
||||||
|
color = theme.BackgroundColor,
|
||||||
|
position = UDim2.new(1, 0, 1, 3),
|
||||||
|
size = self.openBinding:map(function(a)
|
||||||
|
return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0)
|
||||||
|
end),
|
||||||
|
anchorPoint = Vector2.new(1, 0),
|
||||||
|
}, {
|
||||||
|
Border = e(SlicedImage, {
|
||||||
|
slice = Assets.Slices.RoundedBorder,
|
||||||
|
color = theme.BorderColor,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
}),
|
||||||
|
ScrollingFrame = e(ScrollingFrame, {
|
||||||
|
size = UDim2.new(1, -4, 1, -4),
|
||||||
|
position = UDim2.new(0, 2, 0, 2),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
contentSize = self.contentSize,
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Top,
|
||||||
|
FillDirection = Enum.FillDirection.Vertical,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 0),
|
||||||
|
|
||||||
|
[Roact.Change.AbsoluteContentSize] = function(object)
|
||||||
|
self.setContentSize(object.AbsoluteContentSize)
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
Options = Roact.createFragment(optionButtons),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Dropdown
|
||||||
41
plugin/src/App/Components/EditableImage.lua
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local EditableImage = Roact.PureComponent:extend("EditableImage")
|
||||||
|
|
||||||
|
function EditableImage:init()
|
||||||
|
self.ref = Roact.createRef()
|
||||||
|
end
|
||||||
|
|
||||||
|
function EditableImage:writePixels()
|
||||||
|
local image = self.ref.current
|
||||||
|
if not image then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not self.props.pixels then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
image:WritePixels(Vector2.zero, self.props.size, self.props.pixels)
|
||||||
|
end
|
||||||
|
|
||||||
|
function EditableImage:render()
|
||||||
|
return e("EditableImage", {
|
||||||
|
Size = self.props.size,
|
||||||
|
[Roact.Ref] = self.ref,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function EditableImage:didMount()
|
||||||
|
self:writePixels()
|
||||||
|
end
|
||||||
|
|
||||||
|
function EditableImage:didUpdate()
|
||||||
|
self:writePixels()
|
||||||
|
end
|
||||||
|
|
||||||
|
return EditableImage
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
local Flipper = require(Rojo.Flipper)
|
local Flipper = require(Packages.Flipper)
|
||||||
|
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local bindingUtil = require(Plugin.App.bindingUtil)
|
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||||
@@ -29,6 +30,7 @@ function IconButton:render()
|
|||||||
Position = self.props.position,
|
Position = self.props.position,
|
||||||
AnchorPoint = self.props.anchorPoint,
|
AnchorPoint = self.props.anchorPoint,
|
||||||
|
|
||||||
|
Visible = self.props.visible,
|
||||||
LayoutOrder = self.props.layoutOrder,
|
LayoutOrder = self.props.layoutOrder,
|
||||||
ZIndex = self.props.zIndex,
|
ZIndex = self.props.zIndex,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
@@ -36,15 +38,11 @@ function IconButton:render()
|
|||||||
[Roact.Event.Activated] = self.props.onClick,
|
[Roact.Event.Activated] = self.props.onClick,
|
||||||
|
|
||||||
[Roact.Event.MouseEnter] = function()
|
[Roact.Event.MouseEnter] = function()
|
||||||
self.motor:setGoal(
|
self.motor:setGoal(Flipper.Spring.new(1, HOVER_SPRING_PROPS))
|
||||||
Flipper.Spring.new(1, HOVER_SPRING_PROPS)
|
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
|
|
||||||
[Roact.Event.MouseLeave] = function()
|
[Roact.Event.MouseLeave] = function()
|
||||||
self.motor:setGoal(
|
self.motor:setGoal(Flipper.Spring.new(0, HOVER_SPRING_PROPS))
|
||||||
Flipper.Spring.new(0, HOVER_SPRING_PROPS)
|
|
||||||
)
|
|
||||||
end,
|
end,
|
||||||
}, {
|
}, {
|
||||||
Icon = e("ImageLabel", {
|
Icon = e("ImageLabel", {
|
||||||
@@ -73,6 +71,8 @@ function IconButton:render()
|
|||||||
|
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
Children = Roact.createFragment(self.props[Roact.Children]),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
281
plugin/src/App/Components/PatchVisualizer/ChangeList.lua
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
local DisplayValue = require(script.Parent.DisplayValue)
|
||||||
|
|
||||||
|
local EMPTY_TABLE = {}
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local function ViewDiffButton(props)
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
return e("TextButton", {
|
||||||
|
Text = "",
|
||||||
|
Size = UDim2.new(0.7, 0, 1, -4),
|
||||||
|
LayoutOrder = 2,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
[Roact.Event.Activated] = props.onClick,
|
||||||
|
}, {
|
||||||
|
e(BorderedContainer, {
|
||||||
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
transparency = 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 = 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,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function RowContent(props)
|
||||||
|
local values = props.values
|
||||||
|
local metadata = props.metadata
|
||||||
|
|
||||||
|
if props.showStringDiff and values[1] == "Source" then
|
||||||
|
-- Special case for .Source updates
|
||||||
|
return e(ViewDiffButton, {
|
||||||
|
transparency = props.transparency,
|
||||||
|
onClick = function()
|
||||||
|
if not props.showStringDiff then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
props.showStringDiff(tostring(values[2]), tostring(values[3]))
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
if props.showTableDiff and (type(values[2]) == "table" or type(values[3]) == "table") then
|
||||||
|
-- Special case for table properties (like Attributes/Tags)
|
||||||
|
return e(ViewDiffButton, {
|
||||||
|
transparency = props.transparency,
|
||||||
|
onClick = function()
|
||||||
|
if not props.showTableDiff then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
props.showTableDiff(values[2], values[3])
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
return Roact.createFragment({
|
||||||
|
ColumnB = e(
|
||||||
|
"Frame",
|
||||||
|
{
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0.35, 0, 1, 0),
|
||||||
|
LayoutOrder = 2,
|
||||||
|
},
|
||||||
|
e(DisplayValue, {
|
||||||
|
value = values[2],
|
||||||
|
transparency = props.transparency,
|
||||||
|
textColor = if metadata.isWarning
|
||||||
|
then theme.Diff.Warning
|
||||||
|
else theme.Settings.Setting.DescriptionColor,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
ColumnC = e(
|
||||||
|
"Frame",
|
||||||
|
{
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0.35, 0, 1, 0),
|
||||||
|
LayoutOrder = 3,
|
||||||
|
},
|
||||||
|
e(DisplayValue, {
|
||||||
|
value = values[3],
|
||||||
|
transparency = props.transparency,
|
||||||
|
textColor = if metadata.isWarning
|
||||||
|
then theme.Diff.Warning
|
||||||
|
else theme.Settings.Setting.DescriptionColor,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local ChangeList = Roact.Component:extend("ChangeList")
|
||||||
|
|
||||||
|
function ChangeList:init()
|
||||||
|
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
|
end
|
||||||
|
|
||||||
|
function ChangeList:render()
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
local props = self.props
|
||||||
|
local changes = props.changes
|
||||||
|
|
||||||
|
-- Color alternating rows for readability
|
||||||
|
local rowTransparency = props.transparency:map(function(t)
|
||||||
|
return 0.93 + (0.07 * t)
|
||||||
|
end)
|
||||||
|
|
||||||
|
local rows = {}
|
||||||
|
local pad = {
|
||||||
|
PaddingLeft = UDim.new(0, 5),
|
||||||
|
PaddingRight = UDim.new(0, 5),
|
||||||
|
}
|
||||||
|
|
||||||
|
local headerRow = changes[1]
|
||||||
|
local headers = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 24),
|
||||||
|
BackgroundTransparency = rowTransparency,
|
||||||
|
BackgroundColor3 = theme.Diff.Row,
|
||||||
|
LayoutOrder = 0,
|
||||||
|
}, {
|
||||||
|
Padding = e("UIPadding", pad),
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Left,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
}),
|
||||||
|
ColumnA = e("TextLabel", {
|
||||||
|
Text = tostring(headerRow[1]),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.GothamBold,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.TextColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
Size = UDim2.new(0.3, 0, 1, 0),
|
||||||
|
LayoutOrder = 1,
|
||||||
|
}),
|
||||||
|
ColumnB = e("TextLabel", {
|
||||||
|
Text = tostring(headerRow[2]),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.GothamBold,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.TextColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
Size = UDim2.new(0.35, 0, 1, 0),
|
||||||
|
LayoutOrder = 2,
|
||||||
|
}),
|
||||||
|
ColumnC = e("TextLabel", {
|
||||||
|
Text = tostring(headerRow[3]),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.GothamBold,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.TextColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
Size = UDim2.new(0.35, 0, 1, 0),
|
||||||
|
LayoutOrder = 3,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
for row, values in changes do
|
||||||
|
if row == 1 then
|
||||||
|
continue -- Skip headers, already handled above
|
||||||
|
end
|
||||||
|
|
||||||
|
local metadata = values[4] or EMPTY_TABLE
|
||||||
|
local isWarning = metadata.isWarning
|
||||||
|
|
||||||
|
rows[row] = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 24),
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
ColumnA = 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.TextColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
Size = UDim2.new(0.3, 0, 1, 0),
|
||||||
|
LayoutOrder = 1,
|
||||||
|
}),
|
||||||
|
Content = e(RowContent, {
|
||||||
|
values = values,
|
||||||
|
metadata = metadata,
|
||||||
|
transparency = props.transparency,
|
||||||
|
showStringDiff = props.showStringDiff,
|
||||||
|
showTableDiff = props.showTableDiff,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(
|
||||||
|
rows,
|
||||||
|
e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Vertical,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Top,
|
||||||
|
|
||||||
|
[Roact.Change.AbsoluteContentSize] = function(object)
|
||||||
|
self.setContentSize(object.AbsoluteContentSize)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
Headers = headers,
|
||||||
|
Values = e(ScrollingFrame, {
|
||||||
|
size = UDim2.new(1, 0, 1, -24),
|
||||||
|
position = UDim2.new(0, 0, 0, 24),
|
||||||
|
contentSize = self.contentSize,
|
||||||
|
transparency = props.transparency,
|
||||||
|
}, rows),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return ChangeList
|
||||||
126
plugin/src/App/Components/PatchVisualizer/DisplayValue.lua
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local function DisplayValue(props)
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
local t = typeof(props.value)
|
||||||
|
if t == "Color3" then
|
||||||
|
-- Colors get a blot that shows the color
|
||||||
|
return Roact.createFragment({
|
||||||
|
Blot = e("Frame", {
|
||||||
|
BackgroundTransparency = props.transparency,
|
||||||
|
BackgroundColor3 = props.value,
|
||||||
|
Size = UDim2.new(0, 20, 0, 20),
|
||||||
|
Position = UDim2.new(0, 0, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(0, 0.5),
|
||||||
|
}, {
|
||||||
|
Corner = e("UICorner", {
|
||||||
|
CornerRadius = UDim.new(0, 4),
|
||||||
|
}),
|
||||||
|
Stroke = e("UIStroke", {
|
||||||
|
Color = theme.BorderedContainer.BorderColor,
|
||||||
|
Transparency = props.transparency,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Label = e("TextLabel", {
|
||||||
|
Text = string.format("%d, %d, %d", props.value.R * 255, props.value.G * 255, props.value.B * 255),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = props.textColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
Size = UDim2.new(1, -25, 1, 0),
|
||||||
|
Position = UDim2.new(0, 25, 0, 0),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
elseif t == "table" then
|
||||||
|
-- Showing a memory address for tables is useless, so we want to show the best we can
|
||||||
|
local textRepresentation = nil
|
||||||
|
|
||||||
|
local meta = getmetatable(props.value)
|
||||||
|
if meta and meta.__tostring then
|
||||||
|
-- If the table has a tostring metamethod, use that
|
||||||
|
textRepresentation = tostring(props.value)
|
||||||
|
elseif next(props.value) == nil then
|
||||||
|
-- If it's empty, show empty braces
|
||||||
|
textRepresentation = "{}"
|
||||||
|
elseif next(props.value) == 1 then
|
||||||
|
-- We don't need to support mixed tables, so checking the first key is enough
|
||||||
|
-- to determine if it's a simple array
|
||||||
|
local out, i = table.create(#props.value), 0
|
||||||
|
for _, v in props.value do
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
-- Wrap strings in quotes
|
||||||
|
if type(v) == "string" then
|
||||||
|
v = '"' .. v .. '"'
|
||||||
|
end
|
||||||
|
|
||||||
|
out[i] = tostring(v)
|
||||||
|
end
|
||||||
|
textRepresentation = "{ " .. table.concat(out, ", ") .. " }"
|
||||||
|
else
|
||||||
|
-- Otherwise, show the table contents as a dictionary
|
||||||
|
local out, i = {}, 0
|
||||||
|
for k, v in pairs(props.value) do
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
-- Wrap strings in quotes
|
||||||
|
if type(k) == "string" then
|
||||||
|
k = '"' .. k .. '"'
|
||||||
|
end
|
||||||
|
if type(v) == "string" then
|
||||||
|
v = '"' .. v .. '"'
|
||||||
|
end
|
||||||
|
|
||||||
|
out[i] = string.format("[%s] = %s", tostring(k), tostring(v))
|
||||||
|
end
|
||||||
|
textRepresentation = "{ " .. table.concat(out, ", ") .. " }"
|
||||||
|
end
|
||||||
|
|
||||||
|
return e("TextLabel", {
|
||||||
|
Text = textRepresentation,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = props.textColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: Maybe add visualizations to other datatypes?
|
||||||
|
-- Or special text handling tostring for some?
|
||||||
|
-- Will add as needed, let's see what cases arise.
|
||||||
|
|
||||||
|
local textRepresentation = string.gsub(tostring(props.value), "%s", " ")
|
||||||
|
if t == "string" then
|
||||||
|
textRepresentation = '"' .. textRepresentation .. '"'
|
||||||
|
end
|
||||||
|
|
||||||
|
return e("TextLabel", {
|
||||||
|
Text = textRepresentation,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = props.textColor,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return DisplayValue
|
||||||
282
plugin/src/App/Components/PatchVisualizer/DomLabel.lua
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
local SelectionService = game:GetService("Selection")
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
local Flipper = require(Packages.Flipper)
|
||||||
|
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local ChangeList = require(script.Parent.ChangeList)
|
||||||
|
local Tooltip = require(Plugin.App.Components.Tooltip)
|
||||||
|
local ClassIcon = require(Plugin.App.Components.ClassIcon)
|
||||||
|
|
||||||
|
local Expansion = Roact.Component:extend("Expansion")
|
||||||
|
|
||||||
|
function Expansion:render()
|
||||||
|
local props = self.props
|
||||||
|
|
||||||
|
if not props.rendered then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(1, -props.indent, 1, -24),
|
||||||
|
Position = UDim2.new(0, props.indent, 0, 24),
|
||||||
|
}, {
|
||||||
|
ChangeList = e(ChangeList, {
|
||||||
|
changes = props.changeList,
|
||||||
|
transparency = props.transparency,
|
||||||
|
showStringDiff = props.showStringDiff,
|
||||||
|
showTableDiff = props.showTableDiff,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local DomLabel = Roact.Component:extend("DomLabel")
|
||||||
|
|
||||||
|
function DomLabel:init()
|
||||||
|
local initHeight = self.props.elementHeight:getValue()
|
||||||
|
self.expanded = initHeight > 24
|
||||||
|
|
||||||
|
self.motor = Flipper.SingleMotor.new(initHeight)
|
||||||
|
self.binding = bindingUtil.fromMotor(self.motor)
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
renderExpansion = self.expanded,
|
||||||
|
})
|
||||||
|
self.motor:onStep(function(value)
|
||||||
|
local renderExpansion = value > 24
|
||||||
|
|
||||||
|
self.props.setElementHeight(value)
|
||||||
|
if self.props.updateEvent then
|
||||||
|
self.props.updateEvent:Fire()
|
||||||
|
end
|
||||||
|
|
||||||
|
self:setState(function(state)
|
||||||
|
if state.renderExpansion == renderExpansion then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderExpansion = renderExpansion,
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DomLabel:didUpdate(prevProps)
|
||||||
|
if
|
||||||
|
prevProps.instance ~= self.props.instance
|
||||||
|
or prevProps.patchType ~= self.props.patchType
|
||||||
|
or prevProps.name ~= self.props.name
|
||||||
|
or prevProps.changeList ~= self.props.changeList
|
||||||
|
then
|
||||||
|
-- Close the expansion when the domlabel is changed to a different thing
|
||||||
|
self.expanded = false
|
||||||
|
self.motor:setGoal(Flipper.Spring.new(24, {
|
||||||
|
frequency = 5,
|
||||||
|
dampingRatio = 1,
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function DomLabel:render()
|
||||||
|
local props = self.props
|
||||||
|
local depth = props.depth or 1
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
local color = if props.isWarning
|
||||||
|
then theme.Diff.Warning
|
||||||
|
elseif props.patchType then theme.Diff[props.patchType]
|
||||||
|
else theme.TextColor
|
||||||
|
|
||||||
|
local indent = (depth - 1) * 12 + 15
|
||||||
|
|
||||||
|
-- Line guides help indent depth remain readable
|
||||||
|
local lineGuides = {}
|
||||||
|
for i = 2, depth do
|
||||||
|
if props.depthsComplete[i] then
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
if props.isFinalChild and i == depth then
|
||||||
|
-- This line stops halfway down to merge with our connector for the right angle
|
||||||
|
lineGuides["Line_" .. i] = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 2, 0, 15),
|
||||||
|
Position = UDim2.new(0, (12 * (i - 1)) + 6, 0, -1),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundTransparency = props.transparency,
|
||||||
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
-- All other lines go all the way
|
||||||
|
-- with the exception of the final element, which stops halfway down
|
||||||
|
lineGuides["Line_" .. i] = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 2, 1, if props.isFinalElement then -9 else 2),
|
||||||
|
Position = UDim2.new(0, (12 * (i - 1)) + 6, 0, -1),
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundTransparency = props.transparency,
|
||||||
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if depth ~= 1 then
|
||||||
|
lineGuides["Connector"] = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 8, 0, 2),
|
||||||
|
Position = UDim2.new(0, 2 + (12 * props.depth), 0, 12),
|
||||||
|
AnchorPoint = Vector2.xAxis,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundTransparency = props.transparency,
|
||||||
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return e("Frame", {
|
||||||
|
ClipsDescendants = true,
|
||||||
|
BackgroundTransparency = if props.elementIndex % 2 == 0 then 0.985 else 1,
|
||||||
|
BackgroundColor3 = theme.Diff.Row,
|
||||||
|
Size = self.binding:map(function(expand)
|
||||||
|
return UDim2.new(1, 0, 0, expand)
|
||||||
|
end),
|
||||||
|
}, {
|
||||||
|
Padding = e("UIPadding", {
|
||||||
|
PaddingLeft = UDim.new(0, 10),
|
||||||
|
PaddingRight = UDim.new(0, 10),
|
||||||
|
}),
|
||||||
|
Button = e("TextButton", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = "",
|
||||||
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
[Roact.Event.Activated] = function(_rbx: Instance, _input: InputObject, clickCount: number)
|
||||||
|
if clickCount == 1 then
|
||||||
|
-- Double click opens the instance in explorer
|
||||||
|
self.lastDoubleClickTime = os.clock()
|
||||||
|
if props.instance then
|
||||||
|
SelectionService:Set({ props.instance })
|
||||||
|
end
|
||||||
|
elseif clickCount == 0 then
|
||||||
|
-- Single click expands the changes
|
||||||
|
task.wait(0.25)
|
||||||
|
if os.clock() - (self.lastDoubleClickTime or 0) <= 0.25 then
|
||||||
|
-- This is a double click, so don't expand
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if props.changeList then
|
||||||
|
self.expanded = not self.expanded
|
||||||
|
local goalHeight = 24
|
||||||
|
+ (if self.expanded then math.clamp(#props.changeList * 24, 24, 24 * 6) else 0)
|
||||||
|
self.motor:setGoal(Flipper.Spring.new(goalHeight, {
|
||||||
|
frequency = 5,
|
||||||
|
dampingRatio = 1,
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}, {
|
||||||
|
StateTip = if (props.instance or props.changeList)
|
||||||
|
then e(Tooltip.Trigger, {
|
||||||
|
text = (if props.changeList
|
||||||
|
then "Click to " .. (if self.expanded then "hide" else "view") .. " changes"
|
||||||
|
else "") .. (if props.instance
|
||||||
|
then (if props.changeList then " & d" else "D") .. "ouble click to open in Explorer"
|
||||||
|
else ""),
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
}),
|
||||||
|
Expansion = if props.changeList
|
||||||
|
then e(Expansion, {
|
||||||
|
rendered = self.state.renderExpansion,
|
||||||
|
indent = indent,
|
||||||
|
transparency = props.transparency,
|
||||||
|
changeList = props.changeList,
|
||||||
|
showStringDiff = props.showStringDiff,
|
||||||
|
showTableDiff = props.showTableDiff,
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
DiffIcon = if props.patchType
|
||||||
|
then e("ImageLabel", {
|
||||||
|
Image = Assets.Images.Diff[props.patchType],
|
||||||
|
ImageColor3 = color,
|
||||||
|
ImageTransparency = props.transparency,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 14, 0, 14),
|
||||||
|
Position = UDim2.new(0, 0, 0, 12),
|
||||||
|
AnchorPoint = Vector2.new(0, 0.5),
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
ClassIcon = e(ClassIcon, {
|
||||||
|
className = props.className,
|
||||||
|
color = color,
|
||||||
|
transparency = props.transparency,
|
||||||
|
size = UDim2.new(0, 16, 0, 16),
|
||||||
|
position = UDim2.new(0, indent + 2, 0, 12),
|
||||||
|
anchorPoint = Vector2.new(0, 0.5),
|
||||||
|
}),
|
||||||
|
InstanceName = e("TextLabel", {
|
||||||
|
Text = (if props.isWarning then "⚠ " else "") .. props.name,
|
||||||
|
RichText = true,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = if props.patchType then Enum.Font.GothamBold else Enum.Font.GothamMedium,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = color,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
Size = UDim2.new(1, -indent - 50, 0, 24),
|
||||||
|
Position = UDim2.new(0, indent + 22, 0, 0),
|
||||||
|
}),
|
||||||
|
ChangeInfo = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(1, -indent - 80, 0, 24),
|
||||||
|
Position = UDim2.new(1, -2, 0, 0),
|
||||||
|
AnchorPoint = Vector2.new(1, 0),
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
FillDirection = Enum.FillDirection.Horizontal,
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Right,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 4),
|
||||||
|
}),
|
||||||
|
Edits = if props.changeInfo and props.changeInfo.edits
|
||||||
|
then e("TextLabel", {
|
||||||
|
Text = props.changeInfo.edits .. if props.changeInfo.failed then "," else "",
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.Gotham,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.SubTextColor,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
Size = UDim2.new(0, 0, 0, 16),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
LayoutOrder = 2,
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
Failed = if props.changeInfo and props.changeInfo.failed
|
||||||
|
then e("TextLabel", {
|
||||||
|
Text = props.changeInfo.failed,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Font = Enum.Font.Gotham,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Diff.Warning,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
Size = UDim2.new(0, 0, 0, 16),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
LayoutOrder = 6,
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
}),
|
||||||
|
LineGuides = e("Folder", nil, lineGuides),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return DomLabel
|
||||||
152
plugin/src/App/Components/PatchVisualizer/init.lua
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local PatchTree = require(Plugin.PatchTree)
|
||||||
|
local PatchSet = require(Plugin.PatchSet)
|
||||||
|
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
|
||||||
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local DomLabel = require(script.DomLabel)
|
||||||
|
|
||||||
|
local PatchVisualizer = Roact.Component:extend("PatchVisualizer")
|
||||||
|
|
||||||
|
function PatchVisualizer:init()
|
||||||
|
self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
|
||||||
|
|
||||||
|
self.updateEvent = Instance.new("BindableEvent")
|
||||||
|
end
|
||||||
|
|
||||||
|
function PatchVisualizer:willUnmount()
|
||||||
|
self.updateEvent:Destroy()
|
||||||
|
end
|
||||||
|
|
||||||
|
function PatchVisualizer:shouldUpdate(nextProps)
|
||||||
|
if self.props.patchTree ~= nextProps.patchTree then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local currentPatch, nextPatch = self.props.patch, nextProps.patch
|
||||||
|
if currentPatch ~= nil or nextPatch ~= nil then
|
||||||
|
return not PatchSet.isEqual(currentPatch, nextPatch)
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function PatchVisualizer:render()
|
||||||
|
local patchTree = self.props.patchTree
|
||||||
|
if patchTree == nil and self.props.patch ~= nil then
|
||||||
|
patchTree = PatchTree.build(
|
||||||
|
self.props.patch,
|
||||||
|
self.props.instanceMap,
|
||||||
|
self.props.changeListHeaders or { "Property", "Current", "Incoming" }
|
||||||
|
)
|
||||||
|
if self.props.unappliedPatch then
|
||||||
|
patchTree =
|
||||||
|
PatchTree.updateMetadata(patchTree, self.props.patch, self.props.instanceMap, self.props.unappliedPatch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Recusively draw tree
|
||||||
|
local scrollElements, elementHeights, elementIndex = {}, {}, 0
|
||||||
|
|
||||||
|
if patchTree then
|
||||||
|
local elementTotal = patchTree:getCount()
|
||||||
|
local depthsComplete = {}
|
||||||
|
local function drawNode(node, depth)
|
||||||
|
elementIndex += 1
|
||||||
|
|
||||||
|
local parentNode = patchTree:getNode(node.parentId)
|
||||||
|
local isFinalChild = true
|
||||||
|
if parentNode then
|
||||||
|
for _id, sibling in parentNode.children do
|
||||||
|
if type(sibling) == "table" and sibling.name and sibling.name > node.name then
|
||||||
|
isFinalChild = false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local elementHeight, setElementHeight = Roact.createBinding(24)
|
||||||
|
elementHeights[elementIndex] = elementHeight
|
||||||
|
scrollElements[elementIndex] = e(DomLabel, {
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
showStringDiff = self.props.showStringDiff,
|
||||||
|
showTableDiff = self.props.showTableDiff,
|
||||||
|
updateEvent = self.updateEvent,
|
||||||
|
elementHeight = elementHeight,
|
||||||
|
setElementHeight = setElementHeight,
|
||||||
|
elementIndex = elementIndex,
|
||||||
|
isFinalElement = elementIndex == elementTotal,
|
||||||
|
depth = depth,
|
||||||
|
depthsComplete = table.clone(depthsComplete),
|
||||||
|
hasChildren = (node.children ~= nil and next(node.children) ~= nil),
|
||||||
|
isFinalChild = isFinalChild,
|
||||||
|
patchType = node.patchType,
|
||||||
|
className = node.className,
|
||||||
|
isWarning = node.isWarning,
|
||||||
|
instance = node.instance,
|
||||||
|
name = node.name,
|
||||||
|
changeInfo = node.changeInfo,
|
||||||
|
changeList = node.changeList,
|
||||||
|
})
|
||||||
|
|
||||||
|
if isFinalChild then
|
||||||
|
depthsComplete[depth] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
patchTree:forEach(function(node, depth)
|
||||||
|
depthsComplete[depth] = false
|
||||||
|
for i = depth + 1, #depthsComplete do
|
||||||
|
depthsComplete[i] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
drawNode(node, depth)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
return e(BorderedContainer, {
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
size = self.props.size,
|
||||||
|
position = self.props.position,
|
||||||
|
anchorPoint = self.props.anchorPoint,
|
||||||
|
layoutOrder = self.props.layoutOrder,
|
||||||
|
}, {
|
||||||
|
CleanMerge = e("TextLabel", {
|
||||||
|
Visible = #scrollElements == 0,
|
||||||
|
Text = "No changes to sync, project is up to date.",
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 15,
|
||||||
|
TextColor3 = theme.TextColor,
|
||||||
|
TextWrapped = true,
|
||||||
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
VirtualScroller = e(VirtualScroller, {
|
||||||
|
size = UDim2.new(1, 0, 1, -2),
|
||||||
|
position = UDim2.new(0, 0, 0, 2),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
count = #scrollElements,
|
||||||
|
updateEvent = self.updateEvent.Event,
|
||||||
|
render = function(i)
|
||||||
|
return scrollElements[i]
|
||||||
|
end,
|
||||||
|
getHeightBinding = function(i)
|
||||||
|
return elementHeights[i]
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return PatchVisualizer
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
@@ -9,6 +10,12 @@ local bindingUtil = require(Plugin.App.bindingUtil)
|
|||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local scrollDirToAutoSize = {
|
||||||
|
[Enum.ScrollingDirection.X] = Enum.AutomaticSize.X,
|
||||||
|
[Enum.ScrollingDirection.Y] = Enum.AutomaticSize.Y,
|
||||||
|
[Enum.ScrollingDirection.XY] = Enum.AutomaticSize.XY,
|
||||||
|
}
|
||||||
|
|
||||||
local function ScrollingFrame(props)
|
local function ScrollingFrame(props)
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
return e("ScrollingFrame", {
|
return e("ScrollingFrame", {
|
||||||
@@ -22,19 +29,31 @@ 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,
|
||||||
|
|
||||||
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 = if props.contentSize
|
||||||
return UDim2.new(0, 0, 0, value.Y)
|
then props.contentSize:map(function(value)
|
||||||
end),
|
return UDim2.new(
|
||||||
|
0,
|
||||||
|
if (props.scrollingDirection and props.scrollingDirection ~= Enum.ScrollingDirection.Y)
|
||||||
|
then value.X
|
||||||
|
else 0,
|
||||||
|
0,
|
||||||
|
value.Y
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
else UDim2.new(),
|
||||||
|
AutomaticCanvasSize = if props.contentSize == nil
|
||||||
|
then scrollDirToAutoSize[props.scrollingDirection or Enum.ScrollingDirection.XY]
|
||||||
|
else nil,
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ local function SlicedImage(props)
|
|||||||
Size = props.size,
|
Size = props.size,
|
||||||
Position = props.position,
|
Position = props.position,
|
||||||
AnchorPoint = props.anchorPoint,
|
AnchorPoint = props.anchorPoint,
|
||||||
|
AutomaticSize = props.automaticSize,
|
||||||
|
|
||||||
ZIndex = props.zIndex,
|
ZIndex = props.zIndex,
|
||||||
LayoutOrder = props.layoutOrder,
|
LayoutOrder = props.layoutOrder,
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ local RunService = game:GetService("RunService")
|
|||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
|
|||||||
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
|
||||||
205
plugin/src/App/Components/StringDiffVisualizer/init.lua
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
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 Timer = require(Plugin.Timer)
|
||||||
|
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.oldString ~= self.props.oldString or previousProps.newString ~= self.props.newString then
|
||||||
|
self:calculateContentSize()
|
||||||
|
local add, remove = self:calculateDiffLines()
|
||||||
|
self:setState({
|
||||||
|
add = add,
|
||||||
|
remove = remove,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:calculateContentSize()
|
||||||
|
local oldString, newString = self.props.oldString, self.props.newString
|
||||||
|
|
||||||
|
local oldStringBounds = TextService:GetTextSize(oldString, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
||||||
|
local newStringBounds = TextService:GetTextSize(newString, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
|
||||||
|
|
||||||
|
self.setContentSize(
|
||||||
|
Vector2.new(math.max(oldStringBounds.X, newStringBounds.X), math.max(oldStringBounds.Y, newStringBounds.Y))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:calculateDiffLines()
|
||||||
|
Timer.start("StringDiffVisualizer:calculateDiffLines")
|
||||||
|
local oldString, newString = self.props.oldString, self.props.newString
|
||||||
|
|
||||||
|
-- Diff the two texts
|
||||||
|
local startClock = os.clock()
|
||||||
|
local diffs = StringDiff.findDiffs(oldString, newString)
|
||||||
|
local stopClock = os.clock()
|
||||||
|
|
||||||
|
Log.trace(
|
||||||
|
"Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections",
|
||||||
|
#oldString,
|
||||||
|
#newString,
|
||||||
|
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
|
||||||
|
|
||||||
|
Timer.stop()
|
||||||
|
return add, remove
|
||||||
|
end
|
||||||
|
|
||||||
|
function StringDiffVisualizer:render()
|
||||||
|
local oldString, newString = self.props.oldString, self.props.newString
|
||||||
|
|
||||||
|
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 = oldString,
|
||||||
|
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 = newString,
|
||||||
|
lineBackground = theme.Diff.Add,
|
||||||
|
markedLines = self.state.add,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return StringDiffVisualizer
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local Dictionary = require(Plugin.Dictionary)
|
local Dictionary = require(Plugin.Dictionary)
|
||||||
|
|
||||||
@@ -13,7 +14,11 @@ local StudioPluginAction = Roact.Component:extend("StudioPluginAction")
|
|||||||
|
|
||||||
function StudioPluginAction:init()
|
function StudioPluginAction:init()
|
||||||
self.pluginAction = self.props.plugin:CreatePluginAction(
|
self.pluginAction = self.props.plugin:CreatePluginAction(
|
||||||
self.props.name, self.props.title, self.props.description, self.props.icon, self.props.bindable
|
self.props.name,
|
||||||
|
self.props.title,
|
||||||
|
self.props.description,
|
||||||
|
self.props.icon,
|
||||||
|
self.props.bindable
|
||||||
)
|
)
|
||||||
|
|
||||||
self.pluginAction.Triggered:Connect(self.props.onTriggered)
|
self.pluginAction.Triggered:Connect(self.props.onTriggered)
|
||||||
@@ -30,9 +35,12 @@ end
|
|||||||
local function StudioPluginActionWrapper(props)
|
local function StudioPluginActionWrapper(props)
|
||||||
return e(StudioPluginContext.Consumer, {
|
return e(StudioPluginContext.Consumer, {
|
||||||
render = function(plugin)
|
render = function(plugin)
|
||||||
return e(StudioPluginAction, Dictionary.merge(props, {
|
return e(
|
||||||
|
StudioPluginAction,
|
||||||
|
Dictionary.merge(props, {
|
||||||
plugin = plugin,
|
plugin = plugin,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local StudioPluginContext = Roact.createContext(nil)
|
local StudioPluginContext = Roact.createContext(nil)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local Dictionary = require(Plugin.Dictionary)
|
local Dictionary = require(Plugin.Dictionary)
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
local StudioPluginContext = require(script.Parent.StudioPluginContext)
|
local StudioPluginContext = require(script.Parent.StudioPluginContext)
|
||||||
|
|
||||||
@@ -28,11 +32,16 @@ function StudioPluginGui:init()
|
|||||||
self.props.initDockState,
|
self.props.initDockState,
|
||||||
self.props.active,
|
self.props.active,
|
||||||
self.props.overridePreviousState,
|
self.props.overridePreviousState,
|
||||||
floatingSize.X, floatingSize.Y,
|
floatingSize.X,
|
||||||
minimumSize.X, minimumSize.Y
|
floatingSize.Y,
|
||||||
|
minimumSize.X,
|
||||||
|
minimumSize.Y
|
||||||
)
|
)
|
||||||
|
|
||||||
local pluginGui = self.props.plugin:CreateDockWidgetPluginGui(self.props.id, dockWidgetPluginGuiInfo)
|
local pluginGui = self.props.plugin:CreateDockWidgetPluginGui(
|
||||||
|
if self.props.isEphemeral then HttpService:GenerateGUID(false) else self.props.id,
|
||||||
|
dockWidgetPluginGuiInfo
|
||||||
|
)
|
||||||
|
|
||||||
pluginGui.Name = self.props.id
|
pluginGui.Name = self.props.id
|
||||||
pluginGui.Title = self.props.title
|
pluginGui.Title = self.props.title
|
||||||
@@ -56,13 +65,28 @@ end
|
|||||||
function StudioPluginGui:render()
|
function StudioPluginGui:render()
|
||||||
return e(Roact.Portal, {
|
return e(Roact.Portal, {
|
||||||
target = self.pluginGui,
|
target = self.pluginGui,
|
||||||
|
}, {
|
||||||
|
Background = Theme.with(function(theme)
|
||||||
|
return e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
BackgroundColor3 = theme.BackgroundColor,
|
||||||
|
ZIndex = 0,
|
||||||
|
BorderSizePixel = 0,
|
||||||
}, self.props[Roact.Children])
|
}, self.props[Roact.Children])
|
||||||
|
end),
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
function StudioPluginGui:didUpdate(lastProps)
|
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
|
||||||
@@ -74,9 +98,12 @@ end
|
|||||||
local function StudioPluginGuiWrapper(props)
|
local function StudioPluginGuiWrapper(props)
|
||||||
return e(StudioPluginContext.Consumer, {
|
return e(StudioPluginContext.Consumer, {
|
||||||
render = function(plugin)
|
render = function(plugin)
|
||||||
return e(StudioPluginGui, Dictionary.merge(props, {
|
return e(
|
||||||
|
StudioPluginGui,
|
||||||
|
Dictionary.merge(props, {
|
||||||
plugin = plugin,
|
plugin = plugin,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local Dictionary = require(Plugin.Dictionary)
|
local Dictionary = require(Plugin.Dictionary)
|
||||||
|
|
||||||
@@ -17,12 +18,8 @@ StudioToggleButton.defaultProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function StudioToggleButton:init()
|
function StudioToggleButton:init()
|
||||||
local button = self.props.toolbar:CreateButton(
|
local button =
|
||||||
self.props.name,
|
self.props.toolbar:CreateButton(self.props.name, self.props.tooltip, self.props.icon, self.props.text)
|
||||||
self.props.tooltip,
|
|
||||||
self.props.icon,
|
|
||||||
self.props.text
|
|
||||||
)
|
|
||||||
|
|
||||||
button.Click:Connect(function()
|
button.Click:Connect(function()
|
||||||
if self.props.onClick then
|
if self.props.onClick then
|
||||||
@@ -60,9 +57,12 @@ end
|
|||||||
local function StudioToggleButtonWrapper(props)
|
local function StudioToggleButtonWrapper(props)
|
||||||
return e(StudioToolbarContext.Consumer, {
|
return e(StudioToolbarContext.Consumer, {
|
||||||
render = function(toolbar)
|
render = function(toolbar)
|
||||||
return e(StudioToggleButton, Dictionary.merge(props, {
|
return e(
|
||||||
|
StudioToggleButton,
|
||||||
|
Dictionary.merge(props, {
|
||||||
toolbar = toolbar,
|
toolbar = toolbar,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local Dictionary = require(Plugin.Dictionary)
|
local Dictionary = require(Plugin.Dictionary)
|
||||||
|
|
||||||
@@ -35,9 +36,12 @@ end
|
|||||||
local function StudioToolbarWrapper(props)
|
local function StudioToolbarWrapper(props)
|
||||||
return e(StudioPluginContext.Consumer, {
|
return e(StudioPluginContext.Consumer, {
|
||||||
render = function(plugin)
|
render = function(plugin)
|
||||||
return e(StudioToolbar, Dictionary.merge(props, {
|
return e(
|
||||||
|
StudioToolbar,
|
||||||
|
Dictionary.merge(props, {
|
||||||
plugin = plugin,
|
plugin = plugin,
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
local StudioToolbarContext = Roact.createContext(nil)
|
local StudioToolbarContext = Roact.createContext(nil)
|
||||||
|
|
||||||
|
|||||||
195
plugin/src/App/Components/TableDiffVisualizer/Array.lua
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Timer = require(Plugin.Timer)
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
|
local DisplayValue = require(Plugin.App.Components.PatchVisualizer.DisplayValue)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local Array = Roact.Component:extend("Array")
|
||||||
|
|
||||||
|
function Array:init()
|
||||||
|
self:setState({
|
||||||
|
diff = self:calculateDiff(),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function Array:calculateDiff()
|
||||||
|
Timer.start("Array:calculateDiff")
|
||||||
|
--[[
|
||||||
|
Find the indexes that are added or removed from the array,
|
||||||
|
and display them side by side with gaps for the indexes that
|
||||||
|
dont exist in the opposite array.
|
||||||
|
]]
|
||||||
|
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {}
|
||||||
|
|
||||||
|
local i, j = 1, 1
|
||||||
|
local diff = {}
|
||||||
|
|
||||||
|
while i <= #oldTable and j <= #newTable do
|
||||||
|
if oldTable[i] == newTable[j] then
|
||||||
|
table.insert(diff, { oldTable[i], newTable[j] }) -- Unchanged
|
||||||
|
i += 1
|
||||||
|
j += 1
|
||||||
|
elseif not table.find(newTable, oldTable[i], j) then
|
||||||
|
table.insert(diff, { oldTable[i], nil }) -- Removal
|
||||||
|
i += 1
|
||||||
|
elseif not table.find(oldTable, newTable[j], i) then
|
||||||
|
table.insert(diff, { nil, newTable[j] }) -- Addition
|
||||||
|
j += 1
|
||||||
|
else
|
||||||
|
if table.find(newTable, oldTable[i], j) then
|
||||||
|
table.insert(diff, { nil, newTable[j] }) -- Addition
|
||||||
|
j += 1
|
||||||
|
else
|
||||||
|
table.insert(diff, { oldTable[i], nil }) -- Removal
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Handle remaining elements
|
||||||
|
while i <= #oldTable do
|
||||||
|
table.insert(diff, { oldTable[i], nil }) -- Remaining Removals
|
||||||
|
i += 1
|
||||||
|
end
|
||||||
|
while j <= #newTable do
|
||||||
|
table.insert(diff, { nil, newTable[j] }) -- Remaining Additions
|
||||||
|
j += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
Timer.stop()
|
||||||
|
return diff
|
||||||
|
end
|
||||||
|
|
||||||
|
function Array:didUpdate(previousProps)
|
||||||
|
if previousProps.oldTable ~= self.props.oldTable or previousProps.newTable ~= self.props.newTable then
|
||||||
|
self:setState({
|
||||||
|
diff = self:calculateDiff(),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Array:render()
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
local diff = self.state.diff
|
||||||
|
local lines = table.create(#diff)
|
||||||
|
|
||||||
|
for i, element in diff do
|
||||||
|
local oldValue = element[1]
|
||||||
|
local newValue = element[2]
|
||||||
|
|
||||||
|
local patchType = if oldValue == nil then "Add" elseif newValue == nil then "Remove" else "Remain"
|
||||||
|
|
||||||
|
table.insert(
|
||||||
|
lines,
|
||||||
|
e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 25),
|
||||||
|
BackgroundTransparency = if patchType == "Remain" then 1 else self.props.transparency,
|
||||||
|
BackgroundColor3 = if patchType == "Remain" then theme.Diff.Row else theme.Diff[patchType],
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
LayoutOrder = i,
|
||||||
|
}, {
|
||||||
|
DiffIcon = if patchType ~= "Remain"
|
||||||
|
then e("ImageLabel", {
|
||||||
|
Image = Assets.Images.Diff[patchType],
|
||||||
|
ImageColor3 = theme.AddressEntry.PlaceholderColor,
|
||||||
|
ImageTransparency = self.props.transparency,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 15, 0, 15),
|
||||||
|
Position = UDim2.new(0, 7, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(0, 0.5),
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
Old = e("Frame", {
|
||||||
|
Size = UDim2.new(0.5, -30, 1, 0),
|
||||||
|
Position = UDim2.new(0, 30, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
Display = if oldValue ~= nil
|
||||||
|
then e(DisplayValue, {
|
||||||
|
value = oldValue,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
textColor = theme.Settings.Setting.DescriptionColor,
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
}),
|
||||||
|
New = e("Frame", {
|
||||||
|
Size = UDim2.new(0.5, -10, 1, 0),
|
||||||
|
Position = UDim2.new(0.5, 5, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
Display = if newValue ~= nil
|
||||||
|
then e(DisplayValue, {
|
||||||
|
value = newValue,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
textColor = theme.Settings.Setting.DescriptionColor,
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Roact.createFragment({
|
||||||
|
Headers = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 25),
|
||||||
|
BackgroundTransparency = self.props.transparency:map(function(t)
|
||||||
|
return 0.95 + (0.05 * t)
|
||||||
|
end),
|
||||||
|
BackgroundColor3 = theme.Diff.Row,
|
||||||
|
}, {
|
||||||
|
ColumnA = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0.5, -30, 1, 0),
|
||||||
|
Position = UDim2.new(0, 30, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = "Old",
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = Enum.Font.GothamBold,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
}),
|
||||||
|
ColumnB = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0.5, -10, 1, 0),
|
||||||
|
Position = UDim2.new(0.5, 5, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = "New",
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = Enum.Font.GothamBold,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
}),
|
||||||
|
Separator = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 1),
|
||||||
|
Position = UDim2.new(0, 0, 1, 0),
|
||||||
|
BackgroundTransparency = 0,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
KeyValues = e(ScrollingFrame, {
|
||||||
|
position = UDim2.new(0, 1, 0, 25),
|
||||||
|
size = UDim2.new(1, -2, 1, -27),
|
||||||
|
scrollingDirection = Enum.ScrollingDirection.Y,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Top,
|
||||||
|
}),
|
||||||
|
Lines = Roact.createFragment(lines),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Array
|
||||||
211
plugin/src/App/Components/TableDiffVisualizer/Dictionary.lua
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Timer = require(Plugin.Timer)
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
|
local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
|
||||||
|
local DisplayValue = require(Plugin.App.Components.PatchVisualizer.DisplayValue)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local Dictionary = Roact.Component:extend("Dictionary")
|
||||||
|
|
||||||
|
function Dictionary:init()
|
||||||
|
self:setState({
|
||||||
|
diff = self:calculateDiff(),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function Dictionary:calculateDiff()
|
||||||
|
Timer.start("Dictionary:calculateDiff")
|
||||||
|
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {}
|
||||||
|
|
||||||
|
-- Diff the two tables and find the added keys, removed keys, and changed keys
|
||||||
|
local diff = {}
|
||||||
|
|
||||||
|
for key, oldValue in oldTable do
|
||||||
|
local newValue = newTable[key]
|
||||||
|
if newValue == nil then
|
||||||
|
table.insert(diff, {
|
||||||
|
key = key,
|
||||||
|
patchType = "Remove",
|
||||||
|
})
|
||||||
|
elseif newValue ~= oldValue then
|
||||||
|
-- Note: should this do some sort of deep comparison for various types?
|
||||||
|
table.insert(diff, {
|
||||||
|
key = key,
|
||||||
|
patchType = "Edit",
|
||||||
|
})
|
||||||
|
else
|
||||||
|
table.insert(diff, {
|
||||||
|
key = key,
|
||||||
|
patchType = "Remain",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for key in newTable do
|
||||||
|
if oldTable[key] == nil then
|
||||||
|
table.insert(diff, {
|
||||||
|
key = key,
|
||||||
|
patchType = "Add",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(diff, function(a, b)
|
||||||
|
return a.key < b.key
|
||||||
|
end)
|
||||||
|
|
||||||
|
Timer.stop()
|
||||||
|
return diff
|
||||||
|
end
|
||||||
|
|
||||||
|
function Dictionary:didUpdate(previousProps)
|
||||||
|
if previousProps.oldTable ~= self.props.oldTable or previousProps.newTable ~= self.props.newTable then
|
||||||
|
self:setState({
|
||||||
|
diff = self:calculateDiff(),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Dictionary:render()
|
||||||
|
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {}
|
||||||
|
local diff = self.state.diff
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
local lines = table.create(#diff)
|
||||||
|
for order, line in diff do
|
||||||
|
local key = line.key
|
||||||
|
local oldValue = oldTable[key]
|
||||||
|
local newValue = newTable[key]
|
||||||
|
|
||||||
|
table.insert(
|
||||||
|
lines,
|
||||||
|
e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 25),
|
||||||
|
LayoutOrder = order,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundTransparency = if line.patchType == "Remain" then 1 else self.props.transparency,
|
||||||
|
BackgroundColor3 = if line.patchType == "Remain"
|
||||||
|
then theme.Diff.Row
|
||||||
|
else theme.Diff[line.patchType],
|
||||||
|
}, {
|
||||||
|
DiffIcon = if line.patchType ~= "Remain"
|
||||||
|
then e("ImageLabel", {
|
||||||
|
Image = Assets.Images.Diff[line.patchType],
|
||||||
|
ImageColor3 = theme.AddressEntry.PlaceholderColor,
|
||||||
|
ImageTransparency = self.props.transparency,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.new(0, 15, 0, 15),
|
||||||
|
Position = UDim2.new(0, 7, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(0, 0.5),
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
KeyName = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0.3, -15, 1, 0),
|
||||||
|
Position = UDim2.new(0, 30, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = key,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
}),
|
||||||
|
OldValue = e("Frame", {
|
||||||
|
Size = UDim2.new(0.35, -7, 1, 0),
|
||||||
|
Position = UDim2.new(0.3, 15, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
e(DisplayValue, {
|
||||||
|
value = oldValue,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
textColor = theme.Settings.Setting.DescriptionColor,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
NewValue = e("Frame", {
|
||||||
|
Size = UDim2.new(0.35, -8, 1, 0),
|
||||||
|
Position = UDim2.new(0.65, 8, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
e(DisplayValue, {
|
||||||
|
value = newValue,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
textColor = theme.Settings.Setting.DescriptionColor,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Roact.createFragment({
|
||||||
|
Headers = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 25),
|
||||||
|
BackgroundTransparency = self.props.transparency:map(function(t)
|
||||||
|
return 0.95 + (0.05 * t)
|
||||||
|
end),
|
||||||
|
BackgroundColor3 = theme.Diff.Row,
|
||||||
|
}, {
|
||||||
|
ColumnA = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0.3, -15, 1, 0),
|
||||||
|
Position = UDim2.new(0, 30, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = "Key",
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = Enum.Font.GothamBold,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
}),
|
||||||
|
ColumnB = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0.35, -7, 1, 0),
|
||||||
|
Position = UDim2.new(0.3, 15, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = "Old",
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = Enum.Font.GothamBold,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
}),
|
||||||
|
ColumnC = e("TextLabel", {
|
||||||
|
Size = UDim2.new(0.35, -8, 1, 0),
|
||||||
|
Position = UDim2.new(0.65, 8, 0, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Text = "New",
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
Font = Enum.Font.GothamBold,
|
||||||
|
TextSize = 14,
|
||||||
|
TextColor3 = theme.Settings.Setting.DescriptionColor,
|
||||||
|
TextTruncate = Enum.TextTruncate.AtEnd,
|
||||||
|
}),
|
||||||
|
Separator = e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 0, 1),
|
||||||
|
Position = UDim2.new(0, 0, 1, 0),
|
||||||
|
BackgroundTransparency = 0,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
BackgroundColor3 = theme.BorderedContainer.BorderColor,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
KeyValues = e(ScrollingFrame, {
|
||||||
|
position = UDim2.new(0, 1, 0, 25),
|
||||||
|
size = UDim2.new(1, -2, 1, -27),
|
||||||
|
scrollingDirection = Enum.ScrollingDirection.Y,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}, {
|
||||||
|
Layout = e("UIListLayout", {
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Top,
|
||||||
|
}),
|
||||||
|
Lines = Roact.createFragment(lines),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Dictionary
|
||||||
48
plugin/src/App/Components/TableDiffVisualizer/init.lua
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
local Array = require(script:FindFirstChild("Array"))
|
||||||
|
local Dictionary = require(script:FindFirstChild("Dictionary"))
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local TableDiffVisualizer = Roact.Component:extend("TableDiffVisualizer")
|
||||||
|
|
||||||
|
function TableDiffVisualizer:render()
|
||||||
|
local oldTable, newTable = self.props.oldTable or {}, self.props.newTable or {}
|
||||||
|
|
||||||
|
-- Ensure we're diffing tables, not mixing types
|
||||||
|
if type(oldTable) ~= "table" then
|
||||||
|
oldTable = {}
|
||||||
|
end
|
||||||
|
if type(newTable) ~= "table" then
|
||||||
|
newTable = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local isArray = next(newTable) == 1 or next(oldTable) == 1
|
||||||
|
|
||||||
|
return e(BorderedContainer, {
|
||||||
|
size = self.props.size,
|
||||||
|
position = self.props.position,
|
||||||
|
anchorPoint = self.props.anchorPoint,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}, {
|
||||||
|
Content = if isArray
|
||||||
|
then e(Array, {
|
||||||
|
oldTable = oldTable,
|
||||||
|
newTable = newTable,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
})
|
||||||
|
else e(Dictionary, {
|
||||||
|
oldTable = oldTable,
|
||||||
|
newTable = newTable,
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return TableDiffVisualizer
|
||||||
56
plugin/src/App/Components/Tag.lua
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
|
||||||
|
local SlicedImage = require(Plugin.App.Components.SlicedImage)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
return function(props)
|
||||||
|
return e(SlicedImage, {
|
||||||
|
slice = Assets.Slices.RoundedBackground,
|
||||||
|
color = props.color,
|
||||||
|
transparency = props.transparency:map(function(transparency)
|
||||||
|
return 0.9 + (0.1 * transparency)
|
||||||
|
end),
|
||||||
|
layoutOrder = props.layoutOrder,
|
||||||
|
position = props.position,
|
||||||
|
anchorPoint = props.anchorPoint,
|
||||||
|
size = UDim2.new(0, 0, 0, 16),
|
||||||
|
automaticSize = Enum.AutomaticSize.X,
|
||||||
|
}, {
|
||||||
|
Padding = e("UIPadding", {
|
||||||
|
PaddingLeft = UDim.new(0, 4),
|
||||||
|
PaddingRight = UDim.new(0, 4),
|
||||||
|
PaddingTop = UDim.new(0, 2),
|
||||||
|
PaddingBottom = UDim.new(0, 2),
|
||||||
|
}),
|
||||||
|
Icon = if props.icon
|
||||||
|
then e("ImageLabel", {
|
||||||
|
Size = UDim2.new(0, 12, 0, 12),
|
||||||
|
Position = UDim2.new(0, 0, 0.5, 0),
|
||||||
|
AnchorPoint = Vector2.new(0, 0.5),
|
||||||
|
Image = props.icon,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
ImageColor3 = props.color,
|
||||||
|
ImageTransparency = props.transparency,
|
||||||
|
})
|
||||||
|
else nil,
|
||||||
|
Text = e("TextLabel", {
|
||||||
|
Text = props.text,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextSize = 12,
|
||||||
|
TextColor3 = props.color,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Center,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
|
Position = UDim2.new(0, if props.icon then 15 else 0, 0, 0),
|
||||||
|
AutomaticSize = Enum.AutomaticSize.X,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
@@ -2,9 +2,10 @@ local TextService = game:GetService("TextService")
|
|||||||
|
|
||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
local Plugin = Rojo.Plugin
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Packages.Roact)
|
||||||
local Flipper = require(Rojo.Flipper)
|
local Flipper = require(Packages.Flipper)
|
||||||
|
|
||||||
local Theme = require(Plugin.App.Theme)
|
local Theme = require(Plugin.App.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
@@ -40,10 +41,8 @@ end
|
|||||||
|
|
||||||
function TextButton:render()
|
function TextButton:render()
|
||||||
return Theme.with(function(theme)
|
return Theme.with(function(theme)
|
||||||
local textSize = TextService:GetTextSize(
|
local textSize =
|
||||||
self.props.text, 18, Enum.Font.GothamSemibold,
|
TextService:GetTextSize(self.props.text, 18, Enum.Font.GothamMedium, Vector2.new(math.huge, math.huge))
|
||||||
Vector2.new(math.huge, math.huge)
|
|
||||||
)
|
|
||||||
|
|
||||||
local style = self.props.style
|
local style = self.props.style
|
||||||
|
|
||||||
@@ -84,7 +83,7 @@ function TextButton:render()
|
|||||||
|
|
||||||
Text = e("TextLabel", {
|
Text = e("TextLabel", {
|
||||||
Text = self.props.text,
|
Text = self.props.text,
|
||||||
Font = Enum.Font.GothamSemibold,
|
Font = Enum.Font.GothamMedium,
|
||||||
TextSize = 18,
|
TextSize = 18,
|
||||||
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor),
|
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor),
|
||||||
TextTransparency = self.props.transparency,
|
TextTransparency = self.props.transparency,
|
||||||
@@ -123,13 +122,19 @@ function TextButton:render()
|
|||||||
|
|
||||||
Background = style == "Solid" and e(SlicedImage, {
|
Background = style == "Solid" and e(SlicedImage, {
|
||||||
slice = Assets.Slices.RoundedBackground,
|
slice = Assets.Slices.RoundedBackground,
|
||||||
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BackgroundColor, theme.Disabled.BackgroundColor),
|
color = bindingUtil.mapLerp(
|
||||||
|
bindingEnabled,
|
||||||
|
theme.Enabled.BackgroundColor,
|
||||||
|
theme.Disabled.BackgroundColor
|
||||||
|
),
|
||||||
transparency = self.props.transparency,
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
size = UDim2.new(1, 0, 1, 0),
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
|
||||||
zIndex = -2,
|
zIndex = -2,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
Children = Roact.createFragment(self.props[Roact.Children]),
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
107
plugin/src/App/Components/TextInput.lua
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
local Flipper = require(Packages.Flipper)
|
||||||
|
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
local bindingUtil = require(Plugin.App.bindingUtil)
|
||||||
|
|
||||||
|
local SlicedImage = require(script.Parent.SlicedImage)
|
||||||
|
|
||||||
|
local SPRING_PROPS = {
|
||||||
|
frequency = 5,
|
||||||
|
dampingRatio = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local TextInput = Roact.Component:extend("TextInput")
|
||||||
|
|
||||||
|
function TextInput:init()
|
||||||
|
self.motor = Flipper.GroupMotor.new({
|
||||||
|
hover = 0,
|
||||||
|
enabled = self.props.enabled and 1 or 0,
|
||||||
|
})
|
||||||
|
self.binding = bindingUtil.fromMotor(self.motor)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextInput:didUpdate(lastProps)
|
||||||
|
if lastProps.enabled ~= self.props.enabled then
|
||||||
|
self.motor:setGoal({
|
||||||
|
enabled = Flipper.Spring.new(self.props.enabled and 1 or 0),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function TextInput:render()
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
theme = theme.TextInput
|
||||||
|
|
||||||
|
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
|
||||||
|
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
|
||||||
|
|
||||||
|
return e(SlicedImage, {
|
||||||
|
slice = Assets.Slices.RoundedBorder,
|
||||||
|
color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
|
||||||
|
size = self.props.size or UDim2.new(1, 0, 1, 0),
|
||||||
|
position = self.props.position,
|
||||||
|
layoutOrder = self.props.layoutOrder,
|
||||||
|
anchorPoint = self.props.anchorPoint,
|
||||||
|
}, {
|
||||||
|
HoverOverlay = e(SlicedImage, {
|
||||||
|
slice = Assets.Slices.RoundedBackground,
|
||||||
|
color = theme.ActionFillColor,
|
||||||
|
transparency = Roact.joinBindings({
|
||||||
|
hover = bindingHover:map(function(value)
|
||||||
|
return 1 - value
|
||||||
|
end),
|
||||||
|
transparency = self.props.transparency,
|
||||||
|
}):map(function(values)
|
||||||
|
return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
|
||||||
|
end),
|
||||||
|
size = UDim2.new(1, 0, 1, 0),
|
||||||
|
zIndex = -1,
|
||||||
|
}),
|
||||||
|
Input = e("TextBox", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
Text = self.props.text,
|
||||||
|
PlaceholderText = self.props.placeholder,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Disabled.TextColor, theme.Enabled.TextColor),
|
||||||
|
PlaceholderColor3 = bindingUtil.mapLerp(
|
||||||
|
bindingEnabled,
|
||||||
|
theme.Disabled.PlaceholderColor,
|
||||||
|
theme.Enabled.PlaceholderColor
|
||||||
|
),
|
||||||
|
TextSize = 18,
|
||||||
|
TextEditable = self.props.enabled,
|
||||||
|
ClearTextOnFocus = self.props.clearTextOnFocus,
|
||||||
|
|
||||||
|
[Roact.Event.MouseEnter] = function()
|
||||||
|
self.motor:setGoal({
|
||||||
|
hover = Flipper.Spring.new(1, SPRING_PROPS),
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
|
||||||
|
[Roact.Event.MouseLeave] = function()
|
||||||
|
self.motor:setGoal({
|
||||||
|
hover = Flipper.Spring.new(0, SPRING_PROPS),
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
|
||||||
|
[Roact.Event.FocusLost] = function(rbx)
|
||||||
|
self.props.onEntered(rbx.Text)
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
Children = Roact.createFragment(self.props[Roact.Children]),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return TextInput
|
||||||
271
plugin/src/App/Components/Tooltip.lua
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
local TextService = game:GetService("TextService")
|
||||||
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
local Packages = Rojo.Packages
|
||||||
|
|
||||||
|
local Roact = require(Packages.Roact)
|
||||||
|
local Theme = require(Plugin.App.Theme)
|
||||||
|
|
||||||
|
local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local DELAY = 0.75 -- How long to hover before a popup is shown (seconds)
|
||||||
|
local TEXT_PADDING = Vector2.new(8 * 2, 6 * 2) -- Padding for the popup text containers
|
||||||
|
local TAIL_SIZE = 16 -- Size of the triangle tail piece
|
||||||
|
local X_OFFSET = 30 -- How far right (from left) the tail will be (assuming enough space)
|
||||||
|
local Y_OVERLAP = 10 -- Let the triangle tail piece overlap the target a bit to help "connect" it
|
||||||
|
|
||||||
|
local TooltipContext = Roact.createContext({})
|
||||||
|
|
||||||
|
local function Popup(props)
|
||||||
|
local textSize = TextService:GetTextSize(
|
||||||
|
props.Text,
|
||||||
|
16,
|
||||||
|
Enum.Font.GothamMedium,
|
||||||
|
Vector2.new(math.min(props.parentSize.X, 160), math.huge)
|
||||||
|
) + TEXT_PADDING + (Vector2.one * 2)
|
||||||
|
|
||||||
|
local trigger = props.Trigger:getValue()
|
||||||
|
|
||||||
|
local spaceBelow = props.parentSize.Y
|
||||||
|
- (trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y - Y_OVERLAP + TAIL_SIZE)
|
||||||
|
local spaceAbove = trigger.AbsolutePosition.Y + Y_OVERLAP - TAIL_SIZE
|
||||||
|
|
||||||
|
-- If there's not enough space below, and there's more space above, then show the tooltip above the trigger
|
||||||
|
local displayAbove = spaceBelow < textSize.Y and spaceAbove > spaceBelow
|
||||||
|
|
||||||
|
local X = math.clamp(props.Position.X - X_OFFSET, 0, props.parentSize.X - textSize.X)
|
||||||
|
local Y = 0
|
||||||
|
|
||||||
|
if displayAbove then
|
||||||
|
Y = math.max(trigger.AbsolutePosition.Y - TAIL_SIZE - textSize.Y + Y_OVERLAP, 0)
|
||||||
|
else
|
||||||
|
Y = math.min(
|
||||||
|
trigger.AbsolutePosition.Y + trigger.AbsoluteSize.Y + TAIL_SIZE - Y_OVERLAP,
|
||||||
|
props.parentSize.Y - textSize.Y
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Theme.with(function(theme)
|
||||||
|
return e(BorderedContainer, {
|
||||||
|
position = UDim2.fromOffset(X, Y),
|
||||||
|
size = UDim2.fromOffset(textSize.X, textSize.Y),
|
||||||
|
transparency = props.transparency,
|
||||||
|
}, {
|
||||||
|
Label = e("TextLabel", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Position = UDim2.fromScale(0.5, 0.5),
|
||||||
|
Size = UDim2.new(1, -TEXT_PADDING.X, 1, -TEXT_PADDING.Y),
|
||||||
|
AnchorPoint = Vector2.new(0.5, 0.5),
|
||||||
|
Text = props.Text,
|
||||||
|
TextSize = 16,
|
||||||
|
Font = Enum.Font.GothamMedium,
|
||||||
|
TextWrapped = true,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextColor3 = theme.Button.Bordered.Enabled.TextColor,
|
||||||
|
TextTransparency = props.transparency,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Tail = e("ImageLabel", {
|
||||||
|
ZIndex = 100,
|
||||||
|
Position = if displayAbove
|
||||||
|
then UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 1, -1)
|
||||||
|
else UDim2.new(0, math.clamp(props.Position.X - X, 6, textSize.X - 6), 0, -TAIL_SIZE + 1),
|
||||||
|
Size = UDim2.fromOffset(TAIL_SIZE, TAIL_SIZE),
|
||||||
|
AnchorPoint = Vector2.new(0.5, 0),
|
||||||
|
Rotation = if displayAbove then 180 else 0,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Image = "rbxassetid://10983945016",
|
||||||
|
ImageColor3 = theme.BorderedContainer.BackgroundColor,
|
||||||
|
ImageTransparency = props.transparency,
|
||||||
|
}, {
|
||||||
|
Border = e("ImageLabel", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Image = "rbxassetid://10983946430",
|
||||||
|
ImageColor3 = theme.BorderedContainer.BorderColor,
|
||||||
|
ImageTransparency = props.transparency,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local Provider = Roact.Component:extend("TooltipManager")
|
||||||
|
|
||||||
|
function Provider:init()
|
||||||
|
self:setState({
|
||||||
|
tips = {},
|
||||||
|
addTip = function(id: string, data: { Text: string, Position: Vector2, Trigger: any })
|
||||||
|
self:setState(function(state)
|
||||||
|
state.tips[id] = data
|
||||||
|
return state
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
removeTip = function(id: string)
|
||||||
|
self:setState(function(state)
|
||||||
|
state.tips[id] = nil
|
||||||
|
return state
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function Provider:render()
|
||||||
|
return Roact.createElement(TooltipContext.Provider, {
|
||||||
|
value = self.state,
|
||||||
|
}, self.props[Roact.Children])
|
||||||
|
end
|
||||||
|
|
||||||
|
local Container = Roact.Component:extend("TooltipContainer")
|
||||||
|
|
||||||
|
function Container:init()
|
||||||
|
self:setState({
|
||||||
|
size = Vector2.new(200, 100),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function Container:render()
|
||||||
|
return Roact.createElement(TooltipContext.Consumer, {
|
||||||
|
render = function(context)
|
||||||
|
local tips = context.tips
|
||||||
|
local popups = {}
|
||||||
|
|
||||||
|
for key, value in tips do
|
||||||
|
popups[key] = e(Popup, {
|
||||||
|
Text = value.Text or "",
|
||||||
|
Position = value.Position or Vector2.zero,
|
||||||
|
Trigger = value.Trigger,
|
||||||
|
|
||||||
|
parentSize = self.state.size,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return e("Frame", {
|
||||||
|
[Roact.Change.AbsoluteSize] = function(rbx)
|
||||||
|
self:setState({
|
||||||
|
size = rbx.AbsoluteSize,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
ZIndex = 100,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
}, popups)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local Trigger = Roact.Component:extend("TooltipTrigger")
|
||||||
|
|
||||||
|
function Trigger:init()
|
||||||
|
self.id = HttpService:GenerateGUID(false)
|
||||||
|
self.ref = Roact.createRef()
|
||||||
|
self.showingPopup = false
|
||||||
|
|
||||||
|
self.destroy = function()
|
||||||
|
self.props.context.removeTip(self.id)
|
||||||
|
self.showingPopup = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Trigger:willUnmount()
|
||||||
|
if self.showDelayThread then
|
||||||
|
pcall(task.cancel, self.showDelayThread)
|
||||||
|
end
|
||||||
|
if self.destroy then
|
||||||
|
self.destroy()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Trigger:didUpdate(prevProps)
|
||||||
|
if prevProps.text ~= self.props.text then
|
||||||
|
-- Any existing popup is now invalid
|
||||||
|
self.props.context.removeTip(self.id)
|
||||||
|
self.showingPopup = false
|
||||||
|
|
||||||
|
-- Let the new text propagate
|
||||||
|
self:managePopup()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Trigger:isHovering()
|
||||||
|
local rbx = self.ref.current
|
||||||
|
if rbx then
|
||||||
|
return rbx.GuiState == Enum.GuiState.Hover
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function Trigger:getMousePos()
|
||||||
|
local rbx = self.ref.current
|
||||||
|
if rbx then
|
||||||
|
local widget = rbx:FindFirstAncestorOfClass("DockWidgetPluginGui")
|
||||||
|
if widget then
|
||||||
|
return widget:GetRelativeMousePosition()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return Vector2.zero
|
||||||
|
end
|
||||||
|
|
||||||
|
function Trigger:managePopup()
|
||||||
|
if self:isHovering() then
|
||||||
|
if self.showingPopup or self.showDelayThread then
|
||||||
|
-- Don't duplicate popups
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
self.showDelayThread = task.delay(DELAY, function()
|
||||||
|
self.props.context.addTip(self.id, {
|
||||||
|
Text = self.props.text,
|
||||||
|
Position = self:getMousePos(),
|
||||||
|
Trigger = self.ref,
|
||||||
|
})
|
||||||
|
self.showDelayThread = nil
|
||||||
|
self.showingPopup = true
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
if self.showDelayThread then
|
||||||
|
pcall(task.cancel, self.showDelayThread)
|
||||||
|
self.showDelayThread = nil
|
||||||
|
end
|
||||||
|
self.props.context.removeTip(self.id)
|
||||||
|
self.showingPopup = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Trigger:render()
|
||||||
|
local function recalculate()
|
||||||
|
self:managePopup()
|
||||||
|
end
|
||||||
|
|
||||||
|
return e("Frame", {
|
||||||
|
Size = UDim2.fromScale(1, 1),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
ZIndex = self.props.zIndex or 100,
|
||||||
|
[Roact.Ref] = self.ref,
|
||||||
|
|
||||||
|
[Roact.Change.GuiState] = recalculate,
|
||||||
|
[Roact.Change.AbsolutePosition] = recalculate,
|
||||||
|
[Roact.Change.AbsoluteSize] = recalculate,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local function TriggerConsumer(props)
|
||||||
|
return Roact.createElement(TooltipContext.Consumer, {
|
||||||
|
render = function(context)
|
||||||
|
local innerProps = table.clone(props)
|
||||||
|
innerProps.context = context
|
||||||
|
|
||||||
|
return e(Trigger, innerProps)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
Provider = Provider,
|
||||||
|
Container = Container,
|
||||||
|
Trigger = TriggerConsumer,
|
||||||
|
}
|
||||||