Compare commits

..

19 Commits

Author SHA1 Message Date
Micah
5bd3c74db0 Release Rojo v7.4.4 (#964) 2024-08-22 10:06:13 -07:00
Micah
f4e2f5aefc Release Rojo 7.4.3 (#959) 2024-08-06 11:26:00 -07:00
Micah
8ceb40a24e Update rbx-binary to 0.7.6 (#958) 2024-08-06 11:21:41 -07:00
Micah
3e53d67412 In the plugin, don't write properties if they're nil and also a number (#955) 2024-08-02 10:02:32 -07:00
Micah
844f51d916 Update version of aftman used in release workflow 2024-07-23 11:14:18 -07:00
Micah
26974ffd4c Release v7.4.2 (#950) 2024-07-23 11:02:23 -07:00
Micah
91f5b4a675 Update memofs in 7.4.x branch (#949)
Backports the release of memofs v0.3.0
2024-07-23 10:42:32 -07:00
Micah
d179240139 Update rbx_dom for 7.4.x branch (#948) 2024-07-23 10:35:06 -07:00
Micah
67b6a7e198 Backport #917 to Rojo 7.4.x branch (#947) 2024-07-22 12:11:28 -07:00
Micah
3b721242c1 Backport #893 and #903 to Rojo 7.4 (#946)
As part of prep for a 7.4.2 release, this backports changes to the 7.4.X
branch that we can reasonably ship in 7.4.2 without too many code
changes.
2024-07-22 11:55:28 -07:00
EgoMoose
c6ceaa5c87 Trim plugin version string (#889)
This PR is a very small change that fixes the string pattern that reads
the rojo version from `Version.txt`. Currently this reads an extra
new-line character which makes reading the version text in the plugin
difficult.

It seems the rust side of things already trims the string when
comparing, but the lua side does not.

Current:

![pO6gtOXAZq](https://github.com/rojo-rbx/rojo/assets/6201941/1a03fced-f2b5-4a4e-a82d-e11fb0a52af7)

Fix:

![RobloxStudioBeta_GHmiJKAoa3](https://github.com/rojo-rbx/rojo/assets/6201941/3ce711df-fdc6-4f20-8771-5f5118ee013f)

Apologies if I skipped over some process of submitting a bug and / or am
basing on the wrong branch etc.
2024-03-13 09:49:33 -07:00
Kenneth Loeffler
af9629c53f Release 7.4.1 (#872) 2024-02-20 17:41:45 -08:00
Micah
9509909f46 Backport #870 (optional project names) to 7.4.x (#871)
Unlike most of the other backports, this code couldn't be directly
translated so it had to be re-implemented. Luckily, it is very simple.
This implementation is a bit messy and heavy handed with potential
panics, but I think it's probably fine since file names that aren't
UTF-8 aren't really supported anyway. The original implementation is a
lot cleaner though.

The test snapshots are (almost) all identical between the 7.5
implementation and this one. The sole exception is with the path in the
`snapshot_middleware::project` test, since I didn't feel like adding a
`name` parameter to `snapshot_project` in this implementation.
2024-02-20 17:25:05 -08:00
Kenneth Loeffler
88efbd433f Backport #868 to 7.4 (custom pivot geter/setter) (#869)
This PR backports some changes to rbx_dom_lua to fix serving model
pivots
2024-02-20 12:22:27 -08:00
Kenneth Loeffler
f716928683 Add entry for model pivot build fix to 7.4.x changelog (#867) 2024-02-20 12:09:13 -08:00
Kenneth Loeffler
e23d024ba3 Insert Model.NeedsPivotMigration in insert_instance when missing (#865) 2024-02-20 09:11:26 -08:00
Kenneth Loeffler
591419611e Backport #854 to Rojo 7.4 (Lua LF normalization) (#857) 2024-02-14 10:18:46 -08:00
Kenneth Loeffler
f68beab1df Backport #847 to 7.4 (gracefully handle gateway timeouts) (#851)
This PR adds a fix for gateway timeout handling to the 7.4.x branch

Co-authored-by: boatbomber <zack@boatbomber.com>
2024-02-03 20:30:10 -08:00
Kenneth Loeffler
2798610afd Backport #848, #846, #845, #844 to 7.4 (#849)
Co-authored-by: boatbomber <zack@boatbomber.com>
2024-02-01 13:23:51 -08:00
524 changed files with 7166 additions and 36259 deletions

View File

@@ -1,2 +0,0 @@
((nil . ((eglot-luau-rojo-project-path . "plugin.project.json")
(eglot-luau-rojo-sourcemap-enabled . 't))))

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
*.lua linguist-language=Luau

View File

@@ -11,12 +11,12 @@ jobs:
name: Check Actions name: Check Actions
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Changelog check - name: Changelog check
uses: Zomzog/changelog-checker@v1.3.0 uses: Zomzog/changelog-checker@v1.3.0
with: with:
fileName: CHANGELOG.md fileName: CHANGELOG.md
noChangelogLabel: skip changelog noChangelogLabel: skip changelog
checkNotification: Simple checkNotification: Simple
env: env:

View File

@@ -16,24 +16,27 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-22.04, windows-latest, macos-latest, windows-11-arm, ubuntu-22.04-arm] os: [ubuntu-latest, windows-latest, macos-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: actions-rs/toolchain@v1
- name: Restore Rust Cache
uses: actions/cache/restore@v4
with: with:
path: | toolchain: stable
~/.cargo/registry override: true
~/.cargo/git profile: minimal
target
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - 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 - name: Build
run: cargo build --locked --verbose run: cargo build --locked --verbose
@@ -41,75 +44,56 @@ jobs:
- name: Test - name: Test
run: cargo test --locked --verbose run: cargo test --locked --verbose
- name: Save Rust Cache
uses: actions/cache/save@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
msrv: msrv:
name: Check MSRV name: Check MSRV
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@1.88.0 uses: actions-rs/toolchain@v1
- name: Restore Rust Cache
uses: actions/cache/restore@v4
with: with:
path: | toolchain: 1.70.0
~/.cargo/registry override: true
~/.cargo/git profile: minimal
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - 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 - name: Build
run: cargo build --locked --verbose run: cargo build --locked --verbose
- name: Save Rust Cache
uses: actions/cache/save@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
lint: lint:
name: Rustfmt, Clippy, Stylua, & Selene name: Rustfmt, Clippy, Stylua, & Selene
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Install Rust - name: Install Rust
uses: dtolnay/rust-toolchain@stable uses: actions-rs/toolchain@v1
with: with:
toolchain: stable
override: true
components: rustfmt, clippy components: rustfmt, clippy
- name: Restore Rust Cache - name: Rust cache
uses: actions/cache/restore@v4 uses: Swatinem/rust-cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Setup Rokit - name: Setup Aftman
uses: CompeyDev/setup-rokit@v0.1.2 uses: ok-nick/setup-aftman@v0.3.0
with: with:
version: 'v1.1.0' version: 'v0.2.7'
- name: Stylua - name: Stylua
run: stylua --check plugin/src run: stylua --check plugin/src
@@ -123,11 +107,3 @@ jobs:
- name: Clippy - name: Clippy
run: cargo clippy run: cargo clippy
- name: Save Rust Cache
uses: actions/cache/save@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

View File

@@ -8,49 +8,54 @@ 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 }}
run: | with:
gh release create ${{ github.ref_name }} --draft --verify-tag --title ${{ github.ref_name }} tag_name: ${{ github.ref }}
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@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Setup Rokit - name: Setup Aftman
uses: CompeyDev/setup-rokit@v0.1.2 uses: ok-nick/setup-aftman@v0.1.0
with: with:
version: 'v1.1.0' token: ${{ secrets.GITHUB_TOKEN }}
trust-check: false
version: 'v0.3.0'
- name: Build Plugin - name: Build Plugin
run: rojo build plugin.project.json --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 }}
run: | with:
gh release upload ${{ github.ref_name }} Rojo.rbxm upload_url: ${{ needs.create-release.outputs.upload_url }}
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@v4 uses: actions/upload-artifact@v3
with: with:
name: Rojo.rbxm name: Rojo.rbxm
path: Rojo.rbxm path: Rojo.rbxm
- name: Upload Plugin to Roblox
env:
RBX_API_KEY: ${{ secrets.PLUGIN_UPLOAD_TOKEN }}
RBX_UNIVERSE_ID: ${{ vars.PLUGIN_CI_PLACE_ID }}
RBX_PLACE_ID: ${{ vars.PLUGIN_CI_UNIVERSE_ID }}
run: lune run upload-plugin Rojo.rbxm
build: build:
needs: ["create-release"] needs: ["create-release"]
@@ -60,25 +65,15 @@ jobs:
# https://doc.rust-lang.org/rustc/platform-support.html # https://doc.rust-lang.org/rustc/platform-support.html
include: include:
- host: linux - host: linux
os: ubuntu-22.04 os: ubuntu-20.04
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
label: linux-x86_64 label: linux-x86_64
- host: linux
os: ubuntu-22.04-arm
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: windows-x86_64 label: windows-x86_64
- host: windows
os: windows-11-arm
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
@@ -94,64 +89,70 @@ jobs:
env: env:
BIN: rojo BIN: rojo
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
submodules: true submodules: true
- name: Install Rust - name: Get Version from Tag
uses: dtolnay/rust-toolchain@stable shell: bash
with: # https://github.community/t/how-to-get-just-the-tag-name/16241/7#M1027
targets: ${{ matrix.target }} run: |
echo "PROJECT_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
echo "Version is: ${{ env.PROJECT_VERSION }}"
- name: Restore Rust Cache - name: Install Rust
uses: actions/cache/restore@v4 uses: actions-rs/toolchain@v1
with: with:
path: | toolchain: stable
~/.cargo/registry target: ${{ matrix.target }}
~/.cargo/git override: true
target profile: minimal
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Setup Aftman
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 --target ${{ matrix.target }} run: cargo build --release --locked --verbose --target ${{ matrix.target }}
- name: Save Rust Cache
uses: actions/cache/save@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Generate Artifact Name
shell: bash
env: env:
TAG_NAME: ${{ github.ref_name }} # Build into a known directory so we can find our build artifact more
run: | # easily.
echo "ARTIFACT_NAME=$BIN-${TAG_NAME#v}-${{ matrix.label }}.zip" >> "$GITHUB_ENV" CARGO_TARGET_DIR: output
- name: Create Archive and Upload to Release # On platforms that use OpenSSL, ensure it is statically linked to
# make binaries more portable.
OPENSSL_STATIC: 1
- name: Create Release Archive
shell: bash 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 "target/${{ matrix.target }}/release/$BIN.exe" staging/ cp "output/${{ matrix.target }}/release/$BIN.exe" staging/
cd staging cd staging
7z a ../$ARTIFACT_NAME * 7z a ../release.zip *
else else
cp "target/${{ matrix.target }}/release/$BIN" staging/ cp "output/${{ matrix.target }}/release/$BIN" staging/
cd staging cd staging
zip ../$ARTIFACT_NAME * zip ../release.zip *
fi fi
gh release upload ${{ github.ref_name }} ../$ARTIFACT_NAME - name: Upload Archive to Release
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@v4 uses: actions/upload-artifact@v3
with: with:
path: ${{ env.ARTIFACT_NAME }} name: ${{ env.BIN }}-${{ env.PROJECT_VERSION }}-${{ matrix.label }}.zip
name: ${{ env.ARTIFACT_NAME }} path: release.zip

11
.gitignore vendored
View File

@@ -10,8 +10,8 @@
/*.rbxl /*.rbxl
/*.rbxlx /*.rbxlx
# Sourcemap for the Rojo plugin (for better intellisense) # Test places for the Roblox Studio Plugin
/sourcemap.json /plugin/*.rbxlx
# Roblox Studio holds 'lock' files on places # Roblox Studio holds 'lock' files on places
*.rbxl.lock *.rbxl.lock
@@ -19,10 +19,3 @@
# Snapshot files from the 'insta' Rust crate # Snapshot files from the 'insta' Rust crate
**/*.snap.new **/*.snap.new
# Macos file system junk
._*
.DS_STORE
# JetBrains IDEs
/.idea/

6
.gitmodules vendored
View File

@@ -16,9 +16,3 @@
[submodule "plugin/Packages/Highlighter"] [submodule "plugin/Packages/Highlighter"]
path = plugin/Packages/Highlighter path = plugin/Packages/Highlighter
url = https://github.com/boatbomber/highlighter.git url = https://github.com/boatbomber/highlighter.git
[submodule "plugin/Packages/msgpack-luau"]
path = plugin/Packages/msgpack-luau
url = https://github.com/cipharius/msgpack-luau/
[submodule ".lune/opencloud-execute"]
path = .lune/opencloud-execute
url = https://github.com/Dekkonot/opencloud-luau-execute-lune.git

View File

@@ -1,8 +0,0 @@
return {
luau = {
languagemode = "strict",
aliases = {
lune = "~/.lune/.typedefs/0.10.4/",
},
},
}

View File

@@ -1,51 +0,0 @@
local args: any = ...
assert(args, "no arguments passed to script")
local input: buffer = args.BinaryInput
local AssetService = game:GetService("AssetService")
local SerializationService = game:GetService("SerializationService")
local EncodingService = game:GetService("EncodingService")
local input_hash: buffer = EncodingService:ComputeBufferHash(input, Enum.HashAlgorithm.Sha256)
local hex_hash: { string } = table.create(buffer.len(input_hash))
for i = 0, buffer.len(input_hash) - 1 do
table.insert(hex_hash, string.format("%02x", buffer.readu8(input_hash, i)))
end
print(`Deserializing plugin file (size: {buffer.len(input)} bytes, hash: {table.concat(hex_hash, "")})`)
local plugin = SerializationService:DeserializeInstancesAsync(input)[1]
local UploadDetails = require(plugin.UploadDetails) :: any
local PLUGIN_ID = UploadDetails.assetId
local PLUGIN_NAME = UploadDetails.name
local PLUGIN_DESCRIPTION = UploadDetails.description
local PLUGIN_CREATOR_ID = UploadDetails.creatorId
local PLUGIN_CREATOR_TYPE = UploadDetails.creatorType
assert(typeof(PLUGIN_ID) == "number", "UploadDetails did not contain a number field 'assetId'")
assert(typeof(PLUGIN_NAME) == "string", "UploadDetails did not contain a string field 'name'")
assert(typeof(PLUGIN_DESCRIPTION) == "string", "UploadDetails did not contain a string field 'description'")
assert(typeof(PLUGIN_CREATOR_ID) == "number", "UploadDetails did not contain a number field 'creatorId'")
assert(typeof(PLUGIN_CREATOR_TYPE) == "string", "UploadDetails did not contain a string field 'creatorType'")
assert(
Enum.AssetCreatorType:FromName(PLUGIN_CREATOR_TYPE) ~= nil,
"UploadDetails field 'creatorType' was not a valid member of Enum.AssetCreatorType"
)
print(`Uploading to {PLUGIN_ID}`)
print(`Plugin Name: {PLUGIN_NAME}`)
print(`Plugin Description: {PLUGIN_DESCRIPTION}`)
local result, version_or_err = AssetService:CreateAssetVersionAsync(plugin, Enum.AssetType.Plugin, PLUGIN_ID, {
["Name"] = PLUGIN_NAME,
["Description"] = PLUGIN_DESCRIPTION,
["CreatorId"] = PLUGIN_CREATOR_ID,
["CreatorType"] = Enum.AssetCreatorType:FromName(PLUGIN_CREATOR_TYPE),
})
if result ~= Enum.CreateAssetResult.Success then
error(`Plugin failed to upload because: {result.Name} - {version_or_err}`)
end
print(`Plugin uploaded successfully. New version is {version_or_err}.`)

View File

@@ -1,78 +0,0 @@
local fs = require("@lune/fs")
local process = require("@lune/process")
local stdio = require("@lune/stdio")
local luau_execute = require("./opencloud-execute")
local UNIVERSE_ID = process.env["RBX_UNIVERSE_ID"]
local PLACE_ID = process.env["RBX_PLACE_ID"]
local version_string = fs.readFile("plugin/Version.txt")
local versions = { string.match(version_string, "^v?(%d+)%.(%d+)%.(%d+)(.*)$") }
if versions[4] ~= "" then
print("This release is a pre-release. Skipping uploading plugin.")
process.exit(0)
end
local plugin_path = process.args[1]
assert(
typeof(plugin_path) == "string",
"no plugin path provided, expected usage is `lune run upload-plugin [PATH TO RBXM]`."
)
-- For local testing
if process.env["CI"] ~= "true" then
local rojo = process.exec("rojo", { "build", "plugin.project.json", "--output", plugin_path })
if not rojo.ok then
stdio.ewrite("plugin upload failed because: could not build plugin.rbxm\n\n")
stdio.ewrite(rojo.stderr)
stdio.ewrite("\n")
process.exit(1)
end
else
assert(fs.isFile(plugin_path), `Plugin file did not exist at {plugin_path}`)
end
local plugin_content = fs.readFile(plugin_path)
local engine_script = fs.readFile(".lune/scripts/plugin-upload.luau")
print("Creating task to upload plugin")
local task = luau_execute.create_task_latest(UNIVERSE_ID, PLACE_ID, engine_script, 300, false, plugin_content)
print("Waiting for task to finish")
local success = luau_execute.await_finish(task)
if not success then
local error = luau_execute.get_error(task)
assert(error, "could not fetch error from task")
stdio.ewrite("plugin upload failed because: task did not finish successfully\n\n")
stdio.ewrite(error.code)
stdio.ewrite("\n")
stdio.ewrite(error.message)
stdio.ewrite("\n")
process.exit(1)
end
print("Output from task:\n")
for _, log in luau_execute.get_structured_logs(task) do
if log.messageType == "ERROR" then
stdio.write(stdio.color("red"))
stdio.write(log.message)
stdio.write("\n")
stdio.write(stdio.color("reset"))
elseif log.messageType == "INFO" then
stdio.write(stdio.color("cyan"))
stdio.write(log.message)
stdio.write("\n")
stdio.write(stdio.color("reset"))
elseif log.messageType == "WARNING" then
stdio.write(stdio.color("yellow"))
stdio.write(log.message)
stdio.write("\n")
stdio.write(stdio.color("reset"))
else
stdio.write(stdio.color("reset"))
stdio.write(log.message)
stdio.write("\n")
stdio.write(stdio.color("reset"))
end
end

View File

@@ -1,8 +0,0 @@
{
"recommendations": [
"JohnnyMorganz.luau-lsp",
"JohnnyMorganz.stylua",
"Kampfkarren.selene-vscode",
"rust-lang.rust-analyzer"
]
}

View File

@@ -1,4 +0,0 @@
{
"luau-lsp.sourcemap.rojoProjectFile": "plugin.project.json",
"luau-lsp.sourcemap.autogenerate": true
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,31 +14,13 @@ Code contributions are welcome for features and bugs that have been reported in
You'll want these tools to work on Rojo: You'll want these tools to work on Rojo:
* Latest stable Rust compiler * Latest stable Rust compiler
* Rustfmt and Clippy are used for code formatting and linting.
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo) * Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
* [Rokit](https://github.com/rojo-rbx/rokit) * [Foreman](https://github.com/Roblox/foreman)
* [Luau Language Server](https://github.com/JohnnyMorganz/luau-lsp) (Only needed if working on the Studio plugin.)
When working on the Studio plugin, we recommend using this command to automatically rebuild the plugin when you save a change:
*(Make sure you've enabled the Studio setting to reload plugins on file change!)*
```bash
bash scripts/watch-build-plugin.sh
```
You can also run the plugin's unit tests with the following:
*(Make sure you have `run-in-roblox` installed first!)*
```bash
bash scripts/unit-test-plugin.sh
```
## Documentation ## Documentation
Documentation impacts way more people than the individual lines of code we write. Documentation impacts way more people than the individual lines of code we write.
If you find any problems in the documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them. If you find any problems in documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.
## Bug Reports and Feature Requests ## Bug Reports and Feature Requests
Most of the tools around Rojo try to be clear when an issue is a bug. Even if they aren't, sometimes things don't work quite right. Most of the tools around Rojo try to be clear when an issue is a bug. Even if they aren't, sometimes things don't work quite right.

2207
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,8 @@
[package] [package]
name = "rojo" name = "rojo"
version = "7.7.0-rc.1" version = "7.4.4"
rust-version = "1.88" rust-version = "1.70.0"
authors = [ authors = ["Lucien Greathouse <me@lpghatguy.com>"]
"Lucien Greathouse <me@lpghatguy.com>",
"Micah Reid <git@dekkonot.com>",
"Ken Loeffler <kenloef@gmail.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"
homepage = "https://rojo.space" homepage = "https://rojo.space"
@@ -30,9 +26,7 @@ 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 = []
# Run Rojo with this feature to open a Tracy session. profile-with-tracy = ["profiling/profile-with-tracy", "tracy-client"]
# Currently uses protocol v63, last supported in Tracy 0.9.1.
profile-with-tracy = ["profiling/profile-with-tracy"]
[workspace] [workspace]
members = ["crates/*"] members = ["crates/*"]
@@ -46,67 +40,55 @@ name = "build"
harness = false harness = false
[dependencies] [dependencies]
memofs = { version = "0.3.1", 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", features = [ # rbx_binary = { path = "../rbx-dom/rbx_binary" }
# "unstable_text_format",
# ] }
# rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" } # rbx_dom_weak = { path = "../rbx-dom/rbx_dom_weak" }
# rbx_reflection = { path = "../rbx-dom/rbx_reflection" } # rbx_reflection = { path = "../rbx-dom/rbx_reflection" }
# 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 = { version = "2.0.1", features = ["unstable_text_format"] } rbx_binary = "0.7.7"
rbx_dom_weak = "4.1.0" rbx_dom_weak = "2.9.0"
rbx_reflection = "6.1.0" rbx_reflection = "4.7.0"
rbx_reflection_database = "2.0.2" rbx_reflection_database = "0.2.12"
rbx_xml = "2.0.1" rbx_xml = "0.13.5"
anyhow = "1.0.80" anyhow = "1.0.44"
backtrace = "0.3.69" backtrace = "0.3.61"
bincode = "1.3.3" bincode = "1.3.3"
crossbeam-channel = "0.5.12" crossbeam-channel = "0.5.1"
csv = "1.3.0" csv = "1.1.6"
env_logger = "0.9.3" env_logger = "0.9.0"
fs-err = "2.11.0" fs-err = "2.6.0"
futures = "0.3.30" futures = "0.3.17"
globset = "0.4.14" globset = "0.4.8"
humantime = "2.1.0" humantime = "2.1.0"
hyper = { version = "0.14.28", features = ["server", "tcp", "http1"] } hyper = { version = "0.14.13", features = ["server", "tcp", "http1"] }
hyper-tungstenite = "0.11.0"
jod-thread = "0.1.2" jod-thread = "0.1.2"
log = "0.4.21" log = "0.4.14"
num_cpus = "1.16.0" maplit = "1.0.2"
opener = "0.5.2" num_cpus = "1.15.0"
rayon = "1.9.0" opener = "0.5.0"
reqwest = { version = "0.11.24", default-features = false, features = [ rayon = "1.7.0"
reqwest = { version = "0.11.10", features = [
"blocking", "blocking",
"json", "json",
"rustls-tls", "native-tls-vendored",
] } ] }
ritz = "0.1.0" ritz = "0.1.0"
roblox_install = "1.0.0" roblox_install = "1.0.0"
serde = { version = "1.0.197", features = ["derive", "rc"] } serde = { version = "1.0.130", features = ["derive", "rc"] }
serde_json = "1.0.145" serde_json = "1.0.68"
jsonc-parser = { version = "0.27.0", features = ["serde"] } toml = "0.5.9"
strum = { version = "0.27", features = ["derive"] } 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", "macros"] } 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"
yaml-rust2 = "0.10.3"
data-encoding = "2.8.0"
pathdiff = "0.2.3"
blake3 = "1.5.0"
float-cmp = "0.9.0"
indexmap = { version = "2.10.0", features = ["serde"] }
rmp-serde = "1.3.0"
serde_bytes = "0.11.19"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
winreg = "0.10.1" winreg = "0.10.1"
@@ -114,20 +96,20 @@ winreg = "0.10.1"
[build-dependencies] [build-dependencies]
memofs = { version = "0.3.0", path = "crates/memofs" } memofs = { version = "0.3.0", path = "crates/memofs" }
embed-resource = "1.8.0" embed-resource = "1.6.4"
anyhow = "1.0.80" anyhow = "1.0.44"
bincode = "1.3.3" bincode = "1.3.3"
fs-err = "2.11.0" fs-err = "2.6.0"
maplit = "1.0.2" maplit = "1.0.2"
semver = "1.0.22" semver = "1.0.19"
[dev-dependencies] [dev-dependencies]
rojo-insta-ext = { path = "crates/rojo-insta-ext" } rojo-insta-ext = { path = "crates/rojo-insta-ext" }
criterion = "0.3.6" criterion = "0.3.5"
insta = { version = "1.36.1", features = ["redactions", "yaml", "json"] } insta = { version = "1.8.0", features = ["redactions", "yaml"] }
paste = "1.0.14" paste = "1.0.5"
pretty_assertions = "1.4.0" pretty_assertions = "1.2.1"
serde_yaml = "0.8.26" serde_yaml = "0.8.21"
tempfile = "3.10.1" tempfile = "3.2.0"
walkdir = "2.5.0" walkdir = "2.3.2"

View File

@@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<a href="https://rojo.space"><img src="assets/brand_images/logo-512.png" alt="Rojo" height="217" /></a> <a href="https://rojo.space"><img src="assets/logo-512.png" alt="Rojo" height="217" /></a>
</div> </div>
<div>&nbsp;</div> <div>&nbsp;</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.88 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
View File

@@ -0,0 +1,5 @@
[tools]
rojo = "rojo-rbx/rojo@7.4.1"
selene = "Kampfkarren/selene@0.26.1"
stylua = "JohnnyMorganz/stylua@0.18.2"
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"

View File

@@ -1,5 +1,3 @@
# Roblox Studio lock files # Roblox Studio lock files
/*.rbxlx.lock /*.rbxlx.lock
/*.rbxl.lock /*.rbxl.lock
sourcemap.json

View File

@@ -2,4 +2,4 @@ return {
hello = function() hello = function()
print("Hello world, from {project_name}!") print("Hello world, from {project_name}!")
end, end,
} }

View File

@@ -3,6 +3,4 @@
# Roblox Studio lock files # Roblox Studio lock files
/*.rbxlx.lock /*.rbxlx.lock
/*.rbxl.lock /*.rbxl.lock
sourcemap.json

View File

@@ -1,5 +1,3 @@
# Plugin model files # Plugin model files
/{project_name}.rbxmx /{project_name}.rbxmx
/{project_name}.rbxm /{project_name}.rbxm
sourcemap.json

View File

Before

Width:  |  Height:  |  Size: 975 B

After

Width:  |  Height:  |  Size: 975 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 229 B

After

Width:  |  Height:  |  Size: 229 B

View File

Before

Width:  |  Height:  |  Size: 584 B

After

Width:  |  Height:  |  Size: 584 B

View File

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 295 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 607 B

View File

@@ -17,10 +17,6 @@ html {
line-height: 1.4; line-height: 1.4;
} }
body {
background-color: #e7e7e7
}
img { img {
max-width:100%; max-width:100%;
max-height:100%; max-height:100%;

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1 +0,0 @@
print("Hello world, from client!")

View File

@@ -1 +0,0 @@
print("Hello world, from server!")

View File

@@ -1,3 +0,0 @@
return function()
print("Hello, world!")
end

View File

@@ -1 +0,0 @@
print("Hello world, from plugin!")

View File

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 175 B

View File

@@ -20,21 +20,12 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
let file_name = entry.file_name().to_str().unwrap().to_owned(); let file_name = entry.file_name().to_str().unwrap().to_owned();
if file_name.starts_with(".git") {
continue;
}
// We can skip any TestEZ test files since they aren't necessary for // We can skip any TestEZ test files since they aren't necessary for
// the plugin to run. // the plugin to run.
if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") { if file_name.ends_with(".spec.lua") || file_name.ends_with(".spec.luau") {
continue; continue;
} }
// Ignore images in msgpack-luau because they aren't UTF-8 encoded.
if file_name.ends_with(".png") {
continue;
}
let child_snapshot = snapshot_from_fs_path(&entry.path())?; let child_snapshot = snapshot_from_fs_path(&entry.path())?;
children.push((file_name, child_snapshot)); children.push((file_name, child_snapshot));
} }
@@ -50,40 +41,33 @@ fn snapshot_from_fs_path(path: &Path) -> io::Result<VfsSnapshot> {
fn main() -> Result<(), anyhow::Error> { fn main() -> Result<(), anyhow::Error> {
let out_dir = env::var_os("OUT_DIR").unwrap(); let out_dir = env::var_os("OUT_DIR").unwrap();
let root_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let root_dir = env::var_os("CARGO_MANIFEST_DIR").unwrap();
let plugin_dir = root_dir.join("plugin"); let plugin_root = PathBuf::from(root_dir).join("plugin");
let templates_dir = root_dir.join("assets").join("project-templates");
let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?; let our_version = Version::parse(env::var_os("CARGO_PKG_VERSION").unwrap().to_str().unwrap())?;
let plugin_version = let plugin_version =
Version::parse(fs::read_to_string(plugin_dir.join("Version.txt"))?.trim())?; Version::parse(fs::read_to_string(plugin_root.join("Version.txt"))?.trim())?;
assert_eq!( assert_eq!(
our_version, plugin_version, our_version, plugin_version,
"plugin version does not match Cargo version" "plugin version does not match Cargo version"
); );
let template_snapshot = snapshot_from_fs_path(&templates_dir)?; let snapshot = VfsSnapshot::dir(hashmap! {
"default.project.json" => snapshot_from_fs_path(&plugin_root.join("default.project.json"))?,
let plugin_snapshot = VfsSnapshot::dir(hashmap! { "fmt" => snapshot_from_fs_path(&plugin_root.join("fmt"))?,
"default.project.json" => snapshot_from_fs_path(&root_dir.join("plugin.project.json"))?, "http" => snapshot_from_fs_path(&plugin_root.join("http"))?,
"plugin" => VfsSnapshot::dir(hashmap! { "log" => snapshot_from_fs_path(&plugin_root.join("log"))?,
"fmt" => snapshot_from_fs_path(&plugin_dir.join("fmt"))?, "rbx_dom_lua" => snapshot_from_fs_path(&plugin_root.join("rbx_dom_lua"))?,
"http" => snapshot_from_fs_path(&plugin_dir.join("http"))?, "src" => snapshot_from_fs_path(&plugin_root.join("src"))?,
"log" => snapshot_from_fs_path(&plugin_dir.join("log"))?, "Packages" => snapshot_from_fs_path(&plugin_root.join("Packages"))?,
"rbx_dom_lua" => snapshot_from_fs_path(&plugin_dir.join("rbx_dom_lua"))?, "Version.txt" => snapshot_from_fs_path(&plugin_root.join("Version.txt"))?,
"src" => snapshot_from_fs_path(&plugin_dir.join("src"))?,
"Packages" => snapshot_from_fs_path(&plugin_dir.join("Packages"))?,
"Version.txt" => snapshot_from_fs_path(&plugin_dir.join("Version.txt"))?,
"UploadDetails.json" => snapshot_from_fs_path(&plugin_dir.join("UploadDetails.json"))?,
}),
}); });
let template_file = File::create(Path::new(&out_dir).join("templates.bincode"))?; let out_path = Path::new(&out_dir).join("plugin.bincode");
let plugin_file = File::create(Path::new(&out_dir).join("plugin.bincode"))?; let out_file = File::create(out_path)?;
bincode::serialize_into(plugin_file, &plugin_snapshot)?; bincode::serialize_into(out_file, &snapshot)?;
bincode::serialize_into(template_file, &template_snapshot)?;
println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc"); println!("cargo:rerun-if-changed=build/windows/rojo-manifest.rc");
println!("cargo:rerun-if-changed=build/windows/rojo.manifest"); println!("cargo:rerun-if-changed=build/windows/rojo.manifest");

View File

@@ -1,14 +1,6 @@
# memofs Changelog # memofs Changelog
## Unreleased Changes ## Unreleased Changes
* Added `Vfs::canonicalize`. [#1201]
## 0.3.1 (2025-11-27)
* Added `Vfs::exists`. [#1169]
* Added `create_dir` and `create_dir_all` to allow creating directories. [#937]
[#1169]: https://github.com/rojo-rbx/rojo/pull/1169
[#937]: https://github.com/rojo-rbx/rojo/pull/937
## 0.3.0 (2024-03-15) ## 0.3.0 (2024-03-15)
* Changed `StdBackend` file watching component to use minimal recursive watches. [#830] * Changed `StdBackend` file watching component to use minimal recursive watches. [#830]

View File

@@ -1,12 +1,8 @@
[package] [package]
name = "memofs" name = "memofs"
description = "Virtual filesystem with configurable backends." description = "Virtual filesystem with configurable backends."
version = "0.3.1" version = "0.3.0"
authors = [ authors = ["Lucien Greathouse <me@lpghatguy.com>"]
"Lucien Greathouse <me@lpghatguy.com>",
"Micah Reid <git@dekkonot.com>",
"Ken Loeffler <kenloef@gmail.com>",
]
edition = "2018" edition = "2018"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"
@@ -15,10 +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.12" crossbeam-channel = "0.5.1"
fs-err = "2.11.0" fs-err = "2.3.0"
notify = "4.0.17" notify = "4.0.15"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
[dev-dependencies]
tempfile = "3.10.1"

View File

@@ -157,11 +157,6 @@ impl VfsBackend for InMemoryFs {
) )
} }
fn exists(&mut self, path: &Path) -> io::Result<bool> {
let inner = self.inner.lock().unwrap();
Ok(inner.entries.contains_key(path))
}
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> { fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
let inner = self.inner.lock().unwrap(); let inner = self.inner.lock().unwrap();
@@ -181,21 +176,6 @@ impl VfsBackend for InMemoryFs {
} }
} }
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
let mut inner = self.inner.lock().unwrap();
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
}
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
let mut inner = self.inner.lock().unwrap();
let mut path_buf = path.to_path_buf();
while let Some(parent) = path_buf.parent() {
inner.load_snapshot(parent.to_path_buf(), VfsSnapshot::empty_dir())?;
path_buf.pop();
}
inner.load_snapshot(path.to_path_buf(), VfsSnapshot::empty_dir())
}
fn remove_file(&mut self, path: &Path) -> io::Result<()> { fn remove_file(&mut self, path: &Path) -> io::Result<()> {
let mut inner = self.inner.lock().unwrap(); let mut inner = self.inner.lock().unwrap();
@@ -232,33 +212,6 @@ impl VfsBackend for InMemoryFs {
} }
} }
// TODO: We rely on Rojo to prepend cwd to any relative path before storing paths
// in MemoFS. The current implementation will error if no prepended absolute path
// is found. It really only normalizes paths within the provided path's context.
// Example: "/Users/username/project/../other/file.txt" ->
// "/Users/username/other/file.txt"
// Erroneous example: "/Users/../../other/file.txt" -> "/other/file.txt"
// This is not very robust. We should implement proper path normalization here or otherwise
// warn if we are missing context and can not fully canonicalize the path correctly.
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
normalized.pop();
}
std::path::Component::CurDir => {}
_ => normalized.push(component),
}
}
let inner = self.inner.lock().unwrap();
match inner.entries.get(&normalized) {
Some(_) => Ok(normalized),
None => not_found(&normalized),
}
}
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
let inner = self.inner.lock().unwrap(); let inner = self.inner.lock().unwrap();
@@ -275,17 +228,23 @@ impl VfsBackend for InMemoryFs {
} }
fn must_be_file<T>(path: &Path) -> io::Result<T> { fn must_be_file<T>(path: &Path) -> io::Result<T> {
Err(io::Error::other(format!( Err(io::Error::new(
"path {} was a directory, but must be a file", io::ErrorKind::Other,
path.display() format!(
))) "path {} was a directory, but must be a file",
path.display()
),
))
} }
fn must_be_dir<T>(path: &Path) -> io::Result<T> { fn must_be_dir<T>(path: &Path) -> io::Result<T> {
Err(io::Error::other(format!( Err(io::Error::new(
"path {} was a file, but must be a directory", io::ErrorKind::Other,
path.display() format!(
))) "path {} was a file, but must be a directory",
path.display()
),
))
} }
fn not_found<T>(path: &Path) -> io::Result<T> { fn not_found<T>(path: &Path) -> io::Result<T> {

View File

@@ -70,14 +70,10 @@ impl<T> IoResultExt<T> for io::Result<T> {
pub trait VfsBackend: sealed::Sealed + Send + 'static { pub trait VfsBackend: sealed::Sealed + Send + 'static {
fn read(&mut self, path: &Path) -> io::Result<Vec<u8>>; fn read(&mut self, path: &Path) -> io::Result<Vec<u8>>;
fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>; fn write(&mut self, path: &Path, data: &[u8]) -> io::Result<()>;
fn exists(&mut self, path: &Path) -> io::Result<bool>;
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir>; fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir>;
fn create_dir(&mut self, path: &Path) -> io::Result<()>;
fn create_dir_all(&mut self, path: &Path) -> io::Result<()>;
fn metadata(&mut self, path: &Path) -> io::Result<Metadata>; fn metadata(&mut self, path: &Path) -> io::Result<Metadata>;
fn remove_file(&mut self, path: &Path) -> io::Result<()>; fn remove_file(&mut self, path: &Path) -> io::Result<()>;
fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>; fn remove_dir_all(&mut self, path: &Path) -> io::Result<()>;
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf>;
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>; fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent>;
fn watch(&mut self, path: &Path) -> io::Result<()>; fn watch(&mut self, path: &Path) -> io::Result<()>;
@@ -177,11 +173,6 @@ impl VfsInner {
Ok(Arc::new(contents_str.into())) Ok(Arc::new(contents_str.into()))
} }
fn exists<P: AsRef<Path>>(&mut self, path: P) -> io::Result<bool> {
let path = path.as_ref();
self.backend.exists(path)
}
fn write<P: AsRef<Path>, C: AsRef<[u8]>>(&mut self, path: P, contents: C) -> io::Result<()> { 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();
@@ -199,16 +190,6 @@ impl VfsInner {
Ok(dir) Ok(dir)
} }
fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.backend.create_dir(path)
}
fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.backend.create_dir_all(path)
}
fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> { fn remove_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref(); let path = path.as_ref();
let _ = self.backend.unwatch(path); let _ = self.backend.unwatch(path);
@@ -226,11 +207,6 @@ impl VfsInner {
self.backend.metadata(path) self.backend.metadata(path)
} }
fn canonicalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
let path = path.as_ref();
self.backend.canonicalize(path)
}
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
self.backend.event_receiver() self.backend.event_receiver()
} }
@@ -350,42 +326,6 @@ impl Vfs {
self.inner.lock().unwrap().read_dir(path) self.inner.lock().unwrap().read_dir(path)
} }
/// Return whether the given path exists.
///
/// Roughly equivalent to [`std::fs::exists`][std::fs::exists].
///
/// [std::fs::exists]: https://doc.rust-lang.org/stable/std/fs/fn.exists.html
#[inline]
pub fn exists<P: AsRef<Path>>(&self, path: P) -> io::Result<bool> {
let path = path.as_ref();
self.inner.lock().unwrap().exists(path)
}
/// Creates a directory at the provided location.
///
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
/// Similiar to that function, this function will fail if the parent of the
/// path does not exist.
///
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
#[inline]
pub fn create_dir<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.inner.lock().unwrap().create_dir(path)
}
/// Creates a directory at the provided location, recursively creating
/// all parent components if they are missing.
///
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
///
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
#[inline]
pub fn create_dir_all<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.inner.lock().unwrap().create_dir_all(path)
}
/// Remove a file. /// Remove a file.
/// ///
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file]. /// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
@@ -419,19 +359,6 @@ impl Vfs {
self.inner.lock().unwrap().metadata(path) self.inner.lock().unwrap().metadata(path)
} }
/// Normalize a path via the underlying backend.
///
/// Roughly equivalent to [`std::fs::canonicalize`][std::fs::canonicalize]. Relative paths are
/// resolved against the backend's current working directory (if applicable) and errors are
/// surfaced directly from the backend.
///
/// [std::fs::canonicalize]: https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html
#[inline]
pub fn canonicalize<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
let path = path.as_ref();
self.inner.lock().unwrap().canonicalize(path)
}
/// Retrieve a handle to the event receiver for this `Vfs`. /// Retrieve a handle to the event receiver for this `Vfs`.
#[inline] #[inline]
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
@@ -501,31 +428,6 @@ impl VfsLock<'_> {
self.inner.read_dir(path) self.inner.read_dir(path)
} }
/// Creates a directory at the provided location.
///
/// Roughly equivalent to [`std::fs::create_dir`][std::fs::create_dir].
/// Similiar to that function, this function will fail if the parent of the
/// path does not exist.
///
/// [std::fs::create_dir]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir.html
#[inline]
pub fn create_dir<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.inner.create_dir(path)
}
/// Creates a directory at the provided location, recursively creating
/// all parent components if they are missing.
///
/// Roughly equivalent to [`std::fs::create_dir_all`][std::fs::create_dir_all].
///
/// [std::fs::create_dir_all]: https://doc.rust-lang.org/stable/std/fs/fn.create_dir_all.html
#[inline]
pub fn create_dir_all<P: AsRef<Path>>(&mut self, path: P) -> io::Result<()> {
let path = path.as_ref();
self.inner.create_dir_all(path)
}
/// Remove a file. /// Remove a file.
/// ///
/// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file]. /// Roughly equivalent to [`std::fs::remove_file`][std::fs::remove_file].
@@ -559,13 +461,6 @@ impl VfsLock<'_> {
self.inner.metadata(path) self.inner.metadata(path)
} }
/// Normalize a path via the underlying backend.
#[inline]
pub fn normalize<P: AsRef<Path>>(&mut self, path: P) -> io::Result<PathBuf> {
let path = path.as_ref();
self.inner.canonicalize(path)
}
/// Retrieve a handle to the event receiver for this `Vfs`. /// Retrieve a handle to the event receiver for this `Vfs`.
#[inline] #[inline]
pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { pub fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
@@ -581,9 +476,7 @@ impl VfsLock<'_> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::{InMemoryFs, StdBackend, Vfs, VfsSnapshot}; use crate::{InMemoryFs, Vfs, VfsSnapshot};
use std::io;
use std::path::PathBuf;
/// https://github.com/rojo-rbx/rojo/issues/899 /// https://github.com/rojo-rbx/rojo/issues/899
#[test] #[test]
@@ -599,62 +492,4 @@ mod test {
"bar\nfoo\n\n" "bar\nfoo\n\n"
); );
} }
/// https://github.com/rojo-rbx/rojo/issues/1200
#[test]
fn canonicalize_in_memory_success() {
let mut imfs = InMemoryFs::new();
let contents = "Lorem ipsum dolor sit amet.".to_string();
imfs.load_snapshot("/test/file.txt", VfsSnapshot::file(contents.to_string()))
.unwrap();
let vfs = Vfs::new(imfs);
assert_eq!(
vfs.canonicalize("/test/nested/../file.txt").unwrap(),
PathBuf::from("/test/file.txt")
);
assert_eq!(
vfs.read_to_string(vfs.canonicalize("/test/nested/../file.txt").unwrap())
.unwrap()
.to_string(),
contents.to_string()
);
}
#[test]
fn canonicalize_in_memory_missing_errors() {
let imfs = InMemoryFs::new();
let vfs = Vfs::new(imfs);
let err = vfs.canonicalize("test").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn canonicalize_std_backend_success() {
let contents = "Lorem ipsum dolor sit amet.".to_string();
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("file.txt");
fs_err::write(&file_path, contents.to_string()).unwrap();
let vfs = Vfs::new(StdBackend::new());
let canonicalized = vfs.canonicalize(&file_path).unwrap();
assert_eq!(canonicalized, file_path.canonicalize().unwrap());
assert_eq!(
vfs.read_to_string(&canonicalized).unwrap().to_string(),
contents.to_string()
);
}
#[test]
fn canonicalize_std_backend_missing_errors() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("test");
let vfs = Vfs::new(StdBackend::new());
let err = vfs.canonicalize(&file_path).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
} }

View File

@@ -1,5 +1,5 @@
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::Path;
use crate::{Metadata, ReadDir, VfsBackend, VfsEvent}; use crate::{Metadata, ReadDir, VfsBackend, VfsEvent};
@@ -15,43 +15,45 @@ impl NoopBackend {
impl VfsBackend for NoopBackend { impl VfsBackend for NoopBackend {
fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> { fn read(&mut self, _path: &Path) -> io::Result<Vec<u8>> {
Err(io::Error::other("NoopBackend doesn't do anything")) Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
} }
fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> { fn write(&mut self, _path: &Path, _data: &[u8]) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything")) Err(io::Error::new(
} io::ErrorKind::Other,
"NoopBackend doesn't do anything",
fn exists(&mut self, _path: &Path) -> io::Result<bool> { ))
Err(io::Error::other("NoopBackend doesn't do anything"))
} }
fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> { fn read_dir(&mut self, _path: &Path) -> io::Result<ReadDir> {
Err(io::Error::other("NoopBackend doesn't do anything")) Err(io::Error::new(
} io::ErrorKind::Other,
"NoopBackend doesn't do anything",
fn create_dir(&mut self, _path: &Path) -> io::Result<()> { ))
Err(io::Error::other("NoopBackend doesn't do anything"))
}
fn create_dir_all(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything"))
} }
fn remove_file(&mut self, _path: &Path) -> io::Result<()> { fn remove_file(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything")) Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
} }
fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> { fn remove_dir_all(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything")) Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
} }
fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> { fn metadata(&mut self, _path: &Path) -> io::Result<Metadata> {
Err(io::Error::other("NoopBackend doesn't do anything")) Err(io::Error::new(
} io::ErrorKind::Other,
"NoopBackend doesn't do anything",
fn canonicalize(&mut self, _path: &Path) -> io::Result<PathBuf> { ))
Err(io::Error::other("NoopBackend doesn't do anything"))
} }
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
@@ -59,11 +61,17 @@ impl VfsBackend for NoopBackend {
} }
fn watch(&mut self, _path: &Path) -> io::Result<()> { fn watch(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything")) Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
} }
fn unwatch(&mut self, _path: &Path) -> io::Result<()> { fn unwatch(&mut self, _path: &Path) -> io::Result<()> {
Err(io::Error::other("NoopBackend doesn't do anything")) Err(io::Error::new(
io::ErrorKind::Other,
"NoopBackend doesn't do anything",
))
} }
} }

View File

@@ -63,10 +63,6 @@ impl VfsBackend for StdBackend {
fs_err::write(path, data) fs_err::write(path, data)
} }
fn exists(&mut self, path: &Path) -> io::Result<bool> {
std::fs::exists(path)
}
fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> { fn read_dir(&mut self, path: &Path) -> io::Result<ReadDir> {
let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect(); let entries: Result<Vec<_>, _> = fs_err::read_dir(path)?.collect();
let mut entries = entries?; let mut entries = entries?;
@@ -82,14 +78,6 @@ impl VfsBackend for StdBackend {
}) })
} }
fn create_dir(&mut self, path: &Path) -> io::Result<()> {
fs_err::create_dir(path)
}
fn create_dir_all(&mut self, path: &Path) -> io::Result<()> {
fs_err::create_dir_all(path)
}
fn remove_file(&mut self, path: &Path) -> io::Result<()> { fn remove_file(&mut self, path: &Path) -> io::Result<()> {
fs_err::remove_file(path) fs_err::remove_file(path)
} }
@@ -106,10 +94,6 @@ impl VfsBackend for StdBackend {
}) })
} }
fn canonicalize(&mut self, path: &Path) -> io::Result<PathBuf> {
fs_err::canonicalize(path)
}
fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> { fn event_receiver(&self) -> crossbeam_channel::Receiver<VfsEvent> {
self.watcher_receiver.clone() self.watcher_receiver.clone()
} }
@@ -125,13 +109,15 @@ impl VfsBackend for StdBackend {
self.watches.insert(path.to_path_buf()); self.watches.insert(path.to_path_buf());
self.watcher self.watcher
.watch(path, RecursiveMode::Recursive) .watch(path, RecursiveMode::Recursive)
.map_err(io::Error::other) .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.watches.remove(path);
self.watcher.unwatch(path).map_err(io::Error::other) self.watcher
.unwatch(path)
.map_err(|inner| io::Error::new(io::ErrorKind::Other, inner))
} }
} }

View File

@@ -6,5 +6,5 @@ edition = "2018"
publish = false publish = false
[dependencies] [dependencies]
serde = "1.0.197" serde = "1.0.99"
serde_yaml = "0.8.26" serde_yaml = "0.8.9"

View File

@@ -5,13 +5,19 @@ use serde::Serialize;
/// Enables redacting any value that serializes as a string. /// Enables redacting any value that serializes as a string.
/// ///
/// Used for transforming Rojo instance IDs into something deterministic. /// Used for transforming Rojo instance IDs into something deterministic.
#[derive(Default)]
pub struct RedactionMap { pub struct RedactionMap {
ids: HashMap<String, usize>, ids: HashMap<String, usize>,
last_id: usize, last_id: usize,
} }
impl RedactionMap { impl RedactionMap {
pub fn new() -> Self {
Self {
ids: HashMap::new(),
last_id: 0,
}
}
pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> { pub fn get_redacted_value(&self, id: impl ToString) -> Option<String> {
let id = id.to_string(); let id = id.to_string();
@@ -22,12 +28,6 @@ impl RedactionMap {
} }
} }
/// Returns the numeric ID that was assigned to the provided value,
/// if one exists.
pub fn get_id_for_value(&self, value: impl ToString) -> Option<usize> {
self.ids.get(&value.to_string()).cloned()
}
pub fn intern(&mut self, id: impl ToString) { pub fn intern(&mut self, id: impl ToString) {
let last_id = &mut self.last_id; let last_id = &mut self.last_id;

View File

@@ -1,30 +0,0 @@
{
"name": "Rojo",
"tree": {
"$className": "Folder",
"Plugin": {
"$path": "plugin/src"
},
"Packages": {
"$path": "plugin/Packages",
"Log": {
"$path": "plugin/log"
},
"Http": {
"$path": "plugin/http"
},
"Fmt": {
"$path": "plugin/fmt"
},
"RbxDom": {
"$path": "plugin/rbx_dom_lua"
}
},
"Version": {
"$path": "plugin/Version.txt"
},
"UploadDetails": {
"$path": "plugin/UploadDetails.json"
}
}
}

View File

@@ -1,7 +0,0 @@
{
"assetId": 13916111004,
"name": "Rojo",
"description": "The plugin portion of Rojo, a tool to enable professional tooling for Roblox developers.",
"creatorId": 32644114,
"creatorType": "Group"
}

View File

@@ -1 +1 @@
7.7.0-rc.1 7.4.4

View File

@@ -0,0 +1,27 @@
{
"name": "Rojo",
"tree": {
"$className": "Folder",
"Plugin": {
"$path": "src"
},
"Packages": {
"$path": "Packages",
"Log": {
"$path": "log"
},
"Http": {
"$path": "http"
},
"Fmt": {
"$path": "fmt"
},
"RbxDom": {
"$path": "rbx_dom_lua"
}
},
"Version": {
"$path": "Version.txt"
}
}
}

View File

@@ -25,7 +25,7 @@
local function defaultTableDebug(buffer, input) local function defaultTableDebug(buffer, input)
buffer:writeRaw("{") buffer:writeRaw("{")
for key, value in input do for key, value in pairs(input) do
buffer:write("[{:?}] = {:?}", key, value) buffer:write("[{:?}] = {:?}", key, value)
if next(input, key) ~= nil then if next(input, key) ~= nil then
@@ -50,7 +50,7 @@ local function defaultTableDebugExtended(buffer, input)
buffer:writeLineRaw("{") buffer:writeLineRaw("{")
buffer:indent() buffer:indent()
for key, value in input do for key, value in pairs(input) do
buffer:writeLine("[{:?}] = {:#?},", key, value) buffer:writeLine("[{:?}] = {:#?},", key, value)
end end
@@ -70,7 +70,7 @@ local function debugImpl(buffer, value, extendedForm)
elseif valueType == "table" then elseif valueType == "table" then
local valueMeta = getmetatable(value) local valueMeta = getmetatable(value)
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
-- This type implement's the metamethod we made up to line up with -- This type implement's the metamethod we made up to line up with
-- Rust's 'Debug' trait. -- Rust's 'Debug' trait.
@@ -242,4 +242,4 @@ return {
debugOutputBuffer = debugOutputBuffer, debugOutputBuffer = debugOutputBuffer,
fmt = fmt, fmt = fmt,
debugify = debugify, debugify = debugify,
} }

View File

@@ -1,7 +1,5 @@
local HttpService = game:GetService("HttpService") local HttpService = game:GetService("HttpService")
local msgpack = require(script.Parent.Parent.msgpack)
local stringTemplate = [[ local stringTemplate = [[
Http.Response { Http.Response {
code: %d code: %d
@@ -33,8 +31,4 @@ function Response:json()
return HttpService:JSONDecode(self.body) return HttpService:JSONDecode(self.body)
end end
function Response:msgpack() return Response
return msgpack.decode(self.body)
end
return Response

View File

@@ -1,8 +1,7 @@
local HttpService = game:GetService("HttpService") local HttpService = game:GetService("HttpService")
local Log = require(script.Parent.Log)
local msgpack = require(script.Parent.msgpack)
local Promise = require(script.Parent.Promise) local Promise = require(script.Parent.Promise)
local Log = require(script.Parent.Log)
local HttpError = require(script.Error) local HttpError = require(script.Error)
local HttpResponse = require(script.Response) local HttpResponse = require(script.Response)
@@ -69,12 +68,4 @@ function Http.jsonDecode(source)
return HttpService:JSONDecode(source) return HttpService:JSONDecode(source)
end end
function Http.msgpackEncode(object)
return msgpack.encode(object)
end
function Http.msgpackDecode(source)
return msgpack.decode(source)
end
return Http return Http

View File

@@ -2,4 +2,4 @@ return function()
it("should load", function() it("should load", function()
require(script.Parent) require(script.Parent)
end) end)
end end

View File

@@ -57,4 +57,4 @@ function Log.error(template, ...)
error(Fmt.fmt(template, ...)) error(Fmt.fmt(template, ...))
end end
return Log return Log

View File

@@ -2,4 +2,4 @@ return function()
it("should load", function() it("should load", function()
require(script.Parent) require(script.Parent)
end) end)
end end

View File

@@ -188,38 +188,6 @@ types = {
}, },
Content = { Content = {
fromPod = function(pod): Content
if type(pod) == "string" then
if pod == "None" then
return Content.none
else
error(`unexpected Content value '{pod}'`)
end
else
local ty, value = next(pod)
if ty == "Uri" then
return Content.fromUri(value)
elseif ty == "Object" then
error("Object deserializing is not currently implemented")
else
error(`Unknown Content type '{ty}' (could not deserialize)`)
end
end
end,
toPod = function(roblox: Content)
if roblox.SourceType == Enum.ContentSourceType.None then
return "None"
elseif roblox.SourceType == Enum.ContentSourceType.Uri then
return { Uri = roblox.Uri }
elseif roblox.SourceType == Enum.ContentSourceType.Object then
error("Object serializing is not currently implemented")
else
error(`Unknown Content type '{roblox.SourceType} (could not serialize)`)
end
end,
},
ContentId = {
fromPod = identity, fromPod = identity,
toPod = identity, toPod = identity,
}, },
@@ -237,19 +205,6 @@ types = {
end, end,
}, },
EnumItem = {
fromPod = function(pod)
return Enum[pod.type]:FromValue(pod.value)
end,
toPod = function(roblox)
return {
type = tostring(roblox.EnumType),
value = roblox.Value,
}
end,
},
Faces = { Faces = {
fromPod = function(pod) fromPod = function(pod)
local faces = {} local faces = {}
@@ -345,12 +300,7 @@ types = {
local keypoints = {} local keypoints = {}
for index, keypoint in ipairs(pod.keypoints) do for index, keypoint in ipairs(pod.keypoints) do
-- TODO: Add a test for NaN or Infinity values and envelopes keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, keypoint.value, keypoint.envelope)
-- Right now it isn't possible because it'd fail the roundtrip.
-- It's more important that it works right now, though.
local value = keypoint.value or 0
local envelope = keypoint.envelope or 0
keypoints[index] = NumberSequenceKeypoint.new(keypoint.time, value, envelope)
end end
return NumberSequence.new(keypoints) return NumberSequence.new(keypoints)
@@ -378,26 +328,13 @@ types = {
if pod == "Default" then if pod == "Default" then
return nil return nil
else else
-- Passing `nil` instead of not passing anything gives return PhysicalProperties.new(
-- different results, so we have to branch here. pod.density,
if pod.acousticAbsorption then pod.friction,
return (PhysicalProperties.new :: any)( pod.elasticity,
pod.density, pod.frictionWeight,
pod.friction, pod.elasticityWeight
pod.elasticity, )
pod.frictionWeight,
pod.elasticityWeight,
pod.acousticAbsorption
)
else
return PhysicalProperties.new(
pod.density,
pod.friction,
pod.elasticity,
pod.frictionWeight,
pod.elasticityWeight
)
end
end end
end, end,
@@ -411,7 +348,6 @@ types = {
elasticity = roblox.Elasticity, elasticity = roblox.Elasticity,
frictionWeight = roblox.FrictionWeight, frictionWeight = roblox.FrictionWeight,
elasticityWeight = roblox.ElasticityWeight, elasticityWeight = roblox.ElasticityWeight,
acousticAbsorption = roblox.AcousticAbsorption,
} }
end end
end, end,

View File

@@ -5,7 +5,6 @@ Error.Kind = {
UnknownProperty = "UnknownProperty", UnknownProperty = "UnknownProperty",
PropertyNotReadable = "PropertyNotReadable", PropertyNotReadable = "PropertyNotReadable",
PropertyNotWritable = "PropertyNotWritable", PropertyNotWritable = "PropertyNotWritable",
CannotParseBinaryString = "CannotParseBinaryString",
Roblox = "Roblox", Roblox = "Roblox",
} }

View File

@@ -15,12 +15,6 @@
0.0 0.0
] ]
}, },
"TestEnumItem": {
"EnumItem": {
"type": "Material",
"value": 256
}
},
"TestNumber": { "TestNumber": {
"Float64": 1337.0 "Float64": 1337.0
}, },
@@ -176,23 +170,9 @@
}, },
"ty": "ColorSequence" "ty": "ColorSequence"
}, },
"ContentId": { "Content": {
"value": { "value": {
"ContentId": "rbxassetid://12345" "Content": "rbxassetid://12345"
},
"ty": "ContentId"
},
"Content_None": {
"value": {
"Content": "None"
},
"ty": "Content"
},
"Content_Uri": {
"value": {
"Content": {
"Uri": "rbxasset://abc/123.rojo"
}
}, },
"ty": "Content" "ty": "Content"
}, },
@@ -202,15 +182,6 @@
}, },
"ty": "Enum" "ty": "Enum"
}, },
"EnumItem": {
"value": {
"EnumItem": {
"type": "Material",
"value": 256
}
},
"ty": "EnumItem"
},
"Faces": { "Faces": {
"value": { "value": {
"Faces": [ "Faces": [
@@ -441,8 +412,7 @@
"friction": 1.0, "friction": 1.0,
"elasticity": 0.0, "elasticity": 0.0,
"frictionWeight": 50.0, "frictionWeight": 50.0,
"elasticityWeight": 25.0, "elasticityWeight": 25.0
"acousticAbsorption": 0.15625
} }
}, },
"ty": "PhysicalProperties" "ty": "PhysicalProperties"

View File

@@ -1,10 +1,139 @@
local EncodingService = game:GetService("EncodingService") -- Thanks to Tiffany352 for this base64 implementation!
local floor = math.floor
local char = string.char
local function encodeBase64(str)
local out = {}
local nOut = 0
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local strLen = #str
-- 3 octets become 4 hextets
for i = 1, strLen - 2, 3 do
local b1, b2, b3 = str:byte(i, i + 3)
local word = b3 + b2 * 256 + b1 * 256 * 256
local h4 = word % 64 + 1
word = floor(word / 64)
local h3 = word % 64 + 1
word = floor(word / 64)
local h2 = word % 64 + 1
word = floor(word / 64)
local h1 = word % 64 + 1
out[nOut + 1] = alphabet:sub(h1, h1)
out[nOut + 2] = alphabet:sub(h2, h2)
out[nOut + 3] = alphabet:sub(h3, h3)
out[nOut + 4] = alphabet:sub(h4, h4)
nOut = nOut + 4
end
local remainder = strLen % 3
if remainder == 2 then
-- 16 input bits -> 3 hextets (2 full, 1 partial)
local b1, b2 = str:byte(-2, -1)
-- partial is 4 bits long, leaving 2 bits of zero padding ->
-- offset = 4
local word = b2 * 4 + b1 * 4 * 256
local h3 = word % 64 + 1
word = floor(word / 64)
local h2 = word % 64 + 1
word = floor(word / 64)
local h1 = word % 64 + 1
out[nOut + 1] = alphabet:sub(h1, h1)
out[nOut + 2] = alphabet:sub(h2, h2)
out[nOut + 3] = alphabet:sub(h3, h3)
out[nOut + 4] = "="
elseif remainder == 1 then
-- 8 input bits -> 2 hextets (2 full, 1 partial)
local b1 = str:byte(-1, -1)
-- partial is 2 bits long, leaving 4 bits of zero padding ->
-- offset = 16
local word = b1 * 16
local h2 = word % 64 + 1
word = floor(word / 64)
local h1 = word % 64 + 1
out[nOut + 1] = alphabet:sub(h1, h1)
out[nOut + 2] = alphabet:sub(h2, h2)
out[nOut + 3] = "="
out[nOut + 4] = "="
end
-- if the remainder is 0, then no work is needed
return table.concat(out, "")
end
local function decodeBase64(str)
local out = {}
local nOut = 0
local alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local strLen = #str
local acc = 0
local nAcc = 0
local alphabetLut = {}
for i = 1, #alphabet do
alphabetLut[alphabet:sub(i, i)] = i - 1
end
-- 4 hextets become 3 octets
for i = 1, strLen do
local ch = str:sub(i, i)
local byte = alphabetLut[ch]
if byte then
acc = acc * 64 + byte
nAcc = nAcc + 1
end
if nAcc == 4 then
local b3 = acc % 256
acc = floor(acc / 256)
local b2 = acc % 256
acc = floor(acc / 256)
local b1 = acc % 256
out[nOut + 1] = char(b1)
out[nOut + 2] = char(b2)
out[nOut + 3] = char(b3)
nOut = nOut + 3
nAcc = 0
acc = 0
end
end
if nAcc == 3 then
-- 3 hextets -> 16 bit output
acc = acc * 64
acc = floor(acc / 256)
local b2 = acc % 256
acc = floor(acc / 256)
local b1 = acc % 256
out[nOut + 1] = char(b1)
out[nOut + 2] = char(b2)
elseif nAcc == 2 then
-- 2 hextets -> 8 bit output
acc = acc * 64
acc = floor(acc / 256)
acc = acc * 64
acc = floor(acc / 256)
local b1 = acc % 256
out[nOut + 1] = char(b1)
elseif nAcc == 1 then
error("Base64 has invalid length")
end
return table.concat(out, "")
end
return { return {
decode = function(input: string) decode = decodeBase64,
return buffer.tostring(EncodingService:Base64Decode(buffer.fromstring(input))) encode = encodeBase64,
end,
encode = function(input: string)
return buffer.tostring(EncodingService:Base64Encode(buffer.fromstring(input)))
end,
} }

View File

@@ -1,8 +1,6 @@
local CollectionService = game:GetService("CollectionService") local CollectionService = game:GetService("CollectionService")
local ScriptEditorService = game:GetService("ScriptEditorService") local ScriptEditorService = game:GetService("ScriptEditorService")
local Error = require(script.Parent.Error)
--- A list of `Enum.Material` values that are used for Terrain.MaterialColors --- A list of `Enum.Material` values that are used for Terrain.MaterialColors
local TERRAIN_MATERIAL_COLORS = { local TERRAIN_MATERIAL_COLORS = {
Enum.Material.Grass, Enum.Material.Grass,
@@ -53,10 +51,6 @@ return {
return true, instance:GetAttributes() return true, instance:GetAttributes()
end, end,
write = function(instance, _, value) write = function(instance, _, value)
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
local existing = instance:GetAttributes() local existing = instance:GetAttributes()
local didAllWritesSucceed = true local didAllWritesSucceed = true
@@ -166,14 +160,9 @@ return {
return true, colors return true, colors
end, end,
write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 }) write = function(instance: Terrain, _, value: { [Enum.Material]: Color3 })
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
for material, color in value do for material, color in value do
instance:SetMaterialColor(material, color) instance:SetMaterialColor(material, color)
end end
return true return true
end, end,
}, },
@@ -208,30 +197,4 @@ return {
end, end,
}, },
}, },
StyleRule = {
PropertiesSerialize = {
read = function(instance: StyleRule)
return true, instance:GetProperties()
end,
write = function(instance: StyleRule, _, value: { [any]: any })
if typeof(value) ~= "table" then
return false, Error.new(Error.Kind.CannotParseBinaryString)
end
local existing = instance:GetProperties()
for itemName, itemValue in pairs(value) do
instance:SetProperty(itemName, itemValue)
end
for existingItemName in pairs(existing) do
if value[existingItemName] == nil then
instance:SetProperty(existingItemName, nil)
end
end
return true
end,
},
},
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TestEZ = require(ReplicatedStorage.Packages:WaitForChild("TestEZ", 10)) local TestEZ = require(ReplicatedStorage.Packages.TestEZ)
local Rojo = ReplicatedStorage.Rojo local Rojo = ReplicatedStorage.Rojo

View File

@@ -1,5 +1,4 @@
local Packages = script.Parent.Parent.Packages local Packages = script.Parent.Parent.Packages
local HttpService = game:GetService("HttpService")
local Http = require(Packages.Http) local Http = require(Packages.Http)
local Log = require(Packages.Log) local Log = require(Packages.Log)
local Promise = require(Packages.Promise) local Promise = require(Packages.Promise)
@@ -10,9 +9,7 @@ local Version = require(script.Parent.Version)
local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse) local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse) local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
local validateApiSocketPacket = Types.ifEnabled(Types.ApiSocketPacket) local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
local validateApiSerialize = Types.ifEnabled(Types.ApiSerializeResponse)
local validateApiRefPatch = Types.ifEnabled(Types.ApiRefPatchResponse)
local function rejectFailedRequests(response) local function rejectFailedRequests(response)
if response.code >= 400 then if response.code >= 400 then
@@ -48,7 +45,14 @@ end
local function rejectWrongPlaceId(infoResponseBody) local function rejectWrongPlaceId(infoResponseBody)
if infoResponseBody.expectedPlaceIds ~= nil then if infoResponseBody.expectedPlaceIds ~= nil then
local foundId = table.find(infoResponseBody.expectedPlaceIds, game.PlaceId) local foundId = false
for _, id in ipairs(infoResponseBody.expectedPlaceIds) do
if id == game.PlaceId then
foundId = true
break
end
end
if not foundId then if not foundId then
local idList = {} local idList = {}
@@ -58,30 +62,10 @@ local function rejectWrongPlaceId(infoResponseBody)
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 %u, 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(game.PlaceId, table.concat(idList, "\n")) ):format(tostring(game.PlaceId), table.concat(idList, "\n"))
return Promise.reject(message)
end
end
if infoResponseBody.unexpectedPlaceIds ~= nil then
local foundId = table.find(infoResponseBody.unexpectedPlaceIds, game.PlaceId)
if foundId then
local idList = {}
for _, id in ipairs(infoResponseBody.unexpectedPlaceIds) do
table.insert(idList, "- " .. tostring(id))
end
local message = (
"Found a Rojo server, but its project is set to not be used with a specific list of places."
.. "\nYour place ID is %u, but needs to not be one of these:"
.. "\n%s"
.. "\n\nTo change this list, edit 'blockedPlaceIds' in your .project.json file."
):format(game.PlaceId, table.concat(idList, "\n"))
return Promise.reject(message) return Promise.reject(message)
end end
@@ -100,7 +84,6 @@ function ApiContext.new(baseUrl)
__baseUrl = baseUrl, __baseUrl = baseUrl,
__sessionId = nil, __sessionId = nil,
__messageCursor = -1, __messageCursor = -1,
__wsClient = nil,
__connected = true, __connected = true,
__activeRequests = {}, __activeRequests = {},
} }
@@ -128,12 +111,6 @@ function ApiContext:disconnect()
request:cancel() request:cancel()
end end
self.__activeRequests = {} self.__activeRequests = {}
if self.__wsClient then
Log.trace("Closing WebSocket client")
self.__wsClient:Close()
end
self.__wsClient = nil
end end
function ApiContext:setMessageCursor(index) function ApiContext:setMessageCursor(index)
@@ -145,7 +122,7 @@ function ApiContext:connect()
return Http.get(url) return Http.get(url)
:andThen(rejectFailedRequests) :andThen(rejectFailedRequests)
:andThen(Http.Response.msgpack) :andThen(Http.Response.json)
:andThen(rejectWrongProtocolVersion) :andThen(rejectWrongProtocolVersion)
:andThen(function(body) :andThen(function(body)
assert(validateApiInfo(body)) assert(validateApiInfo(body))
@@ -163,7 +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):andThen(rejectFailedRequests):andThen(Http.Response.msgpack):andThen(function(body) return Http.get(url):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
@@ -191,9 +168,9 @@ function ApiContext:write(patch)
table.insert(updated, fixedUpdate) table.insert(updated, fixedUpdate)
end end
-- Only add the 'added' field if the table is non-empty, or else the msgpack -- Only add the 'added' field if the table is non-empty, or else Roblox's
-- encode implementation will turn the table into an array instead of a map, -- JSON implementation will turn the table into an array instead of an
-- causing API validation to fail. -- object, causing API validation to fail.
local added local added
if next(patch.added) ~= nil then if next(patch.added) ~= nil then
added = patch.added added = patch.added
@@ -206,84 +183,54 @@ function ApiContext:write(patch)
added = added, added = added,
} }
body = Http.msgpackEncode(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.msgpack)
:andThen(function(responseBody)
Log.info("Write response: {:?}", responseBody)
return responseBody return responseBody
end) end)
end end
function ApiContext:connectWebSocket(packetHandlers) function ApiContext:retrieveMessages()
local url = ("%s/api/socket/%s"):format(self.__baseUrl, self.__messageCursor) local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
-- Convert HTTP/HTTPS URL to WS/WSS
url = url:gsub("^http://", "ws://"):gsub("^https://", "wss://")
return Promise.new(function(resolve, reject) local function sendRequest()
local success, wsClient = local request = Http.get(url):catch(function(err)
pcall(HttpService.CreateWebStreamClient, HttpService, Enum.WebStreamClientType.WebSocket, { if err.type == Http.Error.Kind.Timeout and self.__connected then
Url = url, return sendRequest()
}) end
if not success then
reject("Failed to create WebSocket client: " .. tostring(wsClient)) return Promise.reject(err)
return 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
return sendRequest():andThen(rejectFailedRequests):andThen(Http.Response.json):andThen(function(body)
if body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID")
end end
self.__wsClient = wsClient
local closed, errored, received assert(validateApiSubscribe(body))
received = self.__wsClient.MessageReceived:Connect(function(msg) self:setMessageCursor(body.messageCursor)
local data = Http.msgpackDecode(msg)
if data.sessionId ~= self.__sessionId then
Log.warn("Received message with wrong session ID; ignoring")
return
end
assert(validateApiSocketPacket(data)) return body.messages
Log.trace("Received websocket packet: {:#?}", data)
local handler = packetHandlers[data.packetType]
if handler then
local ok, err = pcall(handler, data.body)
if not ok then
Log.error("Error in WebSocket packet handler for type '%s': %s", data.packetType, err)
end
else
Log.warn("No handler for WebSocket packet type '%s'", data.packetType)
end
end)
closed = self.__wsClient.Closed:Connect(function()
closed:Disconnect()
errored:Disconnect()
received:Disconnect()
if self.__connected then
reject("WebSocket connection closed unexpectedly")
else
resolve()
end
end)
errored = self.__wsClient.Error:Connect(function(code, msg)
closed:Disconnect()
errored:Disconnect()
received:Disconnect()
reject("WebSocket error: " .. code .. " - " .. msg)
end)
end) end)
end 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, ""):andThen(rejectFailedRequests):andThen(Http.Response.msgpack):andThen(function(body) return Http.post(url, ""):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
@@ -292,40 +239,4 @@ function ApiContext:open(id)
end) end)
end end
function ApiContext:serialize(ids: { string })
local url = ("%s/api/serialize"):format(self.__baseUrl)
local request_body = Http.msgpackEncode({ sessionId = self.__sessionId, ids = ids })
return Http.post(url, request_body)
:andThen(rejectFailedRequests)
:andThen(Http.Response.msgpack)
:andThen(function(response_body)
if response_body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID")
end
assert(validateApiSerialize(response_body))
return response_body
end)
end
function ApiContext:refPatch(ids: { string })
local url = ("%s/api/ref-patch"):format(self.__baseUrl)
local request_body = Http.msgpackEncode({ sessionId = self.__sessionId, ids = ids })
return Http.post(url, request_body)
:andThen(rejectFailedRequests)
:andThen(Http.Response.msgpack)
:andThen(function(response_body)
if response_body.sessionId ~= self.__sessionId then
return Promise.reject("Server changed ID")
end
assert(validateApiRefPatch(response_body))
return response_body
end)
end
return ApiContext return ApiContext

View File

@@ -32,7 +32,7 @@ end
function Checkbox:render() function Checkbox:render()
return Theme.with(function(theme) return Theme.with(function(theme)
local checkboxTheme = theme.Checkbox theme = theme.Checkbox
local activeTransparency = Roact.joinBindings({ local activeTransparency = Roact.joinBindings({
self.binding:map(function(value) self.binding:map(function(value)
@@ -57,21 +57,20 @@ function Checkbox:render()
end, end,
}, { }, {
StateTip = e(Tooltip.Trigger, { StateTip = e(Tooltip.Trigger, {
text = (if self.props.locked text = (if self.props.locked then "[LOCKED] " else "")
then (self.props.lockedTooltip or "(Cannot be changed right now)") .. "\n" .. (if self.props.active then "Enabled" else "Disabled"),
else "") .. (if self.props.active then "Enabled" else "Disabled"),
}), }),
Active = e(SlicedImage, { Active = e(SlicedImage, {
slice = Assets.Slices.RoundedBackground, slice = Assets.Slices.RoundedBackground,
color = checkboxTheme.Active.BackgroundColor, color = theme.Active.BackgroundColor,
transparency = activeTransparency, transparency = activeTransparency,
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
zIndex = 2, zIndex = 2,
}, { }, {
Icon = e("ImageLabel", { Icon = e("ImageLabel", {
Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Active, Image = if self.props.locked then Assets.Images.Checkbox.Locked else Assets.Images.Checkbox.Active,
ImageColor3 = checkboxTheme.Active.IconColor, ImageColor3 = theme.Active.IconColor,
ImageTransparency = activeTransparency, ImageTransparency = activeTransparency,
Size = UDim2.new(0, 16, 0, 16), Size = UDim2.new(0, 16, 0, 16),
@@ -84,7 +83,7 @@ function Checkbox:render()
Inactive = e(SlicedImage, { Inactive = e(SlicedImage, {
slice = Assets.Slices.RoundedBorder, slice = Assets.Slices.RoundedBorder,
color = checkboxTheme.Inactive.BorderColor, color = theme.Inactive.BorderColor,
transparency = self.props.transparency, transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
}, { }, {
@@ -92,7 +91,7 @@ function Checkbox:render()
Image = if self.props.locked Image = if self.props.locked
then Assets.Images.Checkbox.Locked then Assets.Images.Checkbox.Locked
else Assets.Images.Checkbox.Inactive, else Assets.Images.Checkbox.Inactive,
ImageColor3 = checkboxTheme.Inactive.IconColor, ImageColor3 = theme.Inactive.IconColor,
ImageTransparency = self.props.transparency, ImageTransparency = self.props.transparency,
Size = UDim2.new(0, 16, 0, 16), Size = UDim2.new(0, 16, 0, 16),

View File

@@ -1,160 +0,0 @@
local StudioService = game:GetService("StudioService")
local AssetService = game:GetService("AssetService")
type CachedImageInfo = {
pixels: buffer,
size: Vector2,
}
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local e = Roact.createElement
local EditableImage = require(Plugin.App.Components.EditableImage)
local imageCache: { [string]: CachedImageInfo } = {}
local function cloneBuffer(b: buffer): buffer
local newBuffer = buffer.create(buffer.len(b))
buffer.copy(newBuffer, 0, b)
return newBuffer
end
local function getImageSizeAndPixels(image: string): (Vector2, buffer)
local cachedImage = imageCache[image]
if not cachedImage then
local editableImage = AssetService:CreateEditableImageAsync(Content.fromUri(image))
local size = editableImage.Size
local pixels = editableImage:ReadPixelsBuffer(Vector2.zero, size)
imageCache[image] = {
pixels = pixels,
size = size,
}
return size, cloneBuffer(pixels)
end
return cachedImage.size, cloneBuffer(cachedImage.pixels)
end
local function getRecoloredClassIcon(className, color)
local iconProps = StudioService:GetClassIcon(className)
if iconProps and color then
--stylua: ignore
local success, editableImageSize, editableImagePixels = pcall(function(_iconProps: { [any]: any }, _color: Color3): (Vector2, buffer)
local size, pixels = getImageSizeAndPixels(_iconProps.Image)
local pixelsLen = buffer.len(pixels)
local minVal, maxVal = math.huge, -math.huge
for i = 0, pixelsLen, 4 do
if buffer.readu8(pixels, i + 3) == 0 then
continue
end
local pixelVal = math.max(
buffer.readu8(pixels, i),
buffer.readu8(pixels, i + 1),
buffer.readu8(pixels, i + 2)
)
minVal = math.min(minVal, pixelVal)
maxVal = math.max(maxVal, pixelVal)
end
local hue, sat, val = _color:ToHSV()
for i = 0, pixelsLen, 4 do
if buffer.readu8(pixels, i + 3) == 0 then
continue
end
local gIndex = i + 1
local bIndex = i + 2
local pixelVal = math.max(
buffer.readu8(pixels, i),
buffer.readu8(pixels, gIndex),
buffer.readu8(pixels, bIndex)
)
local newVal = val
if minVal < maxVal then
-- Remap minVal - maxVal to val*0.9 - val
newVal = val * (0.9 + 0.1 * (pixelVal - minVal) / (maxVal - minVal))
end
local newPixelColor = Color3.fromHSV(hue, sat, newVal)
buffer.writeu8(pixels, i, newPixelColor.R)
buffer.writeu8(pixels, gIndex, newPixelColor.G)
buffer.writeu8(pixels, bIndex, newPixelColor.B)
end
return size, pixels
end, iconProps, color)
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

View 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

View File

@@ -1,3 +1,5 @@
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 Packages = Rojo.Packages
@@ -8,11 +10,9 @@ 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 getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
local SlicedImage = require(script.Parent.SlicedImage) local SlicedImage = require(script.Parent.SlicedImage)
local ScrollingFrame = require(script.Parent.ScrollingFrame) local ScrollingFrame = require(script.Parent.ScrollingFrame)
local Tooltip = require(script.Parent.Tooltip)
local e = Roact.createElement local e = Roact.createElement
@@ -44,29 +44,29 @@ end
function Dropdown:render() function Dropdown:render()
return Theme.with(function(theme) return Theme.with(function(theme)
local dropdownTheme = theme.Dropdown theme = theme.Dropdown
local optionButtons = {} local optionButtons = {}
local width = -1 local width = -1
for i, option in self.props.options do for i, option in self.props.options do
local text = tostring(option or "") local text = tostring(option or "")
local textBounds = getTextBoundsAsync(text, theme.Font.Main, theme.TextSize.Body, math.huge) local textSize = TextService:GetTextSize(text, 15, Enum.Font.GothamMedium, Vector2.new(math.huge, 20))
if textBounds.X > width then if textSize.X > width then
width = textBounds.X width = textSize.X
end end
optionButtons[text] = e("TextButton", { optionButtons[text] = e("TextButton", {
Text = text, Text = text,
LayoutOrder = i, LayoutOrder = i,
Size = UDim2.new(1, 0, 0, 24), Size = UDim2.new(1, 0, 0, 24),
BackgroundColor3 = dropdownTheme.BackgroundColor, BackgroundColor3 = theme.BackgroundColor,
TextTransparency = self.props.transparency, TextTransparency = self.props.transparency,
BackgroundTransparency = self.props.transparency, BackgroundTransparency = self.props.transparency,
BorderSizePixel = 0, BorderSizePixel = 0,
TextColor3 = dropdownTheme.TextColor, TextColor3 = theme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextSize = theme.TextSize.Body, TextSize = 15,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
[Roact.Event.Activated] = function() [Roact.Event.Activated] = function()
if self.props.locked then if self.props.locked then
@@ -103,13 +103,15 @@ function Dropdown:render()
}, { }, {
Border = e(SlicedImage, { Border = e(SlicedImage, {
slice = Assets.Slices.RoundedBorder, slice = Assets.Slices.RoundedBorder,
color = dropdownTheme.BorderColor, color = theme.BorderColor,
transparency = self.props.transparency, transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
}, { }, {
DropArrow = e("ImageLabel", { DropArrow = e("ImageLabel", {
Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow, Image = if self.props.locked then Assets.Images.Dropdown.Locked else Assets.Images.Dropdown.Arrow,
ImageColor3 = dropdownTheme.IconColor, ImageColor3 = self.openBinding:map(function(a)
return theme.Closed.IconColor:Lerp(theme.Open.IconColor, a)
end),
ImageTransparency = self.props.transparency, ImageTransparency = self.props.transparency,
Size = UDim2.new(0, 18, 0, 18), Size = UDim2.new(0, 18, 0, 18),
@@ -120,21 +122,15 @@ function Dropdown:render()
end), end),
BackgroundTransparency = 1, BackgroundTransparency = 1,
}, {
StateTip = if self.props.locked
then e(Tooltip.Trigger, {
text = self.props.lockedTooltip or "(Cannot be changed right now)",
})
else nil,
}), }),
Active = e("TextLabel", { Active = e("TextLabel", {
Size = UDim2.new(1, -30, 1, 0), Size = UDim2.new(1, -30, 1, 0),
Position = UDim2.new(0, 6, 0, 0), Position = UDim2.new(0, 6, 0, 0),
BackgroundTransparency = 1, BackgroundTransparency = 1,
Text = self.props.active, Text = self.props.active,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Body, TextSize = 15,
TextColor3 = dropdownTheme.TextColor, TextColor3 = theme.TextColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = self.props.transparency, TextTransparency = self.props.transparency,
}), }),
@@ -142,7 +138,7 @@ function Dropdown:render()
Options = if self.state.open Options = if self.state.open
then e(SlicedImage, { then e(SlicedImage, {
slice = Assets.Slices.RoundedBackground, slice = Assets.Slices.RoundedBackground,
color = dropdownTheme.BackgroundColor, color = theme.BackgroundColor,
position = UDim2.new(1, 0, 1, 3), position = UDim2.new(1, 0, 1, 3),
size = self.openBinding:map(function(a) size = self.openBinding:map(function(a)
return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0) return UDim2.new(1, 0, a * math.min(3, #self.props.options), 0)
@@ -151,7 +147,7 @@ function Dropdown:render()
}, { }, {
Border = e(SlicedImage, { Border = e(SlicedImage, {
slice = Assets.Slices.RoundedBorder, slice = Assets.Slices.RoundedBorder,
color = dropdownTheme.BorderColor, color = theme.BorderColor,
transparency = self.props.transparency, transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
}), }),

View File

@@ -1,42 +0,0 @@
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 :: EditableImage
if not image then
return
end
if not self.props.pixels then
return
end
image:WritePixelsBuffer(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

View File

@@ -9,70 +9,8 @@ local Assets = require(Plugin.Assets)
local Config = require(Plugin.Config) local Config = require(Plugin.Config)
local Version = require(Plugin.Version) local Version = require(Plugin.Version)
local Tooltip = require(Plugin.App.Components.Tooltip)
local SlicedImage = require(script.Parent.SlicedImage)
local e = Roact.createElement local e = Roact.createElement
local function VersionIndicator(props)
local updateMessage = Version.getUpdateMessage()
return Theme.with(function(theme)
return e("Frame", {
LayoutOrder = props.layoutOrder,
Size = UDim2.new(0, 0, 0, 25),
BackgroundTransparency = 1,
AutomaticSize = Enum.AutomaticSize.X,
}, {
Border = if updateMessage
then e(SlicedImage, {
slice = Assets.Slices.RoundedBorder,
color = theme.Button.Bordered.Enabled.BorderColor,
transparency = props.transparency,
size = UDim2.fromScale(1, 1),
zIndex = 0,
}, {
Indicator = e("ImageLabel", {
Size = UDim2.new(0, 10, 0, 10),
ScaleType = Enum.ScaleType.Fit,
Image = Assets.Images.Circles[16],
ImageColor3 = theme.Header.LogoColor,
ImageTransparency = props.transparency,
BackgroundTransparency = 1,
Position = UDim2.new(1, 0, 0, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
}),
})
else nil,
Tip = if updateMessage
then e(Tooltip.Trigger, {
text = updateMessage,
delay = 0.1,
})
else nil,
VersionText = e("TextLabel", {
Text = Version.display(Config.version),
FontFace = theme.Font.Thin,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Header.VersionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
BackgroundTransparency = 1,
Size = UDim2.new(0, 0, 1, 0),
AutomaticSize = Enum.AutomaticSize.X,
}, {
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 6),
PaddingRight = UDim.new(0, 6),
}),
}),
})
end)
end
local function Header(props) local function Header(props)
return Theme.with(function(theme) return Theme.with(function(theme)
return e("Frame", { return e("Frame", {
@@ -91,9 +29,18 @@ local function Header(props)
BackgroundTransparency = 1, BackgroundTransparency = 1,
}), }),
VersionIndicator = e(VersionIndicator, { Version = e("TextLabel", {
transparency = props.transparency, Text = Version.display(Config.version),
layoutOrder = 2, Font = Enum.Font.Gotham,
TextSize = 14,
TextColor3 = theme.Header.VersionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
Size = UDim2.new(1, 0, 0, 14),
LayoutOrder = 2,
BackgroundTransparency = 1,
}), }),
Layout = e("UIListLayout", { Layout = e("UIListLayout", {

View File

@@ -1,157 +0,0 @@
local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local Log = require(Packages.Log)
local Theme = require(Plugin.App.Theme)
local Assets = require(Plugin.Assets)
local TextButton = require(Plugin.App.Components.TextButton)
local e = Roact.createElement
local FullscreenNotification = Roact.Component:extend("FullscreeFullscreenNotificationnNotification")
function FullscreenNotification:init()
self.transparency, self.setTransparency = Roact.createBinding(0)
self.lifetime = self.props.timeout
self.dismissed = false
end
function FullscreenNotification:dismiss()
if self.dismissed then
return
end
self.dismissed = true
if self.props.onClose then
self.props.onClose()
end
end
function FullscreenNotification:didMount()
self.props.soundPlayer:play(Assets.Sounds.Notification)
self.timeout = task.spawn(function()
local clock = os.clock()
local seen = false
while task.wait(1 / 10) do
local now = os.clock()
local dt = now - clock
clock = now
if not seen then
seen = StudioService.ActiveScript == nil
end
if not seen then
-- Don't run down timer before being viewed
continue
end
self.lifetime -= dt
if self.lifetime <= 0 then
self:dismiss()
break
end
end
self.timeout = nil
end)
end
function FullscreenNotification:willUnmount()
if self.timeout and coroutine.status(self.timeout) == "suspended" then
task.cancel(self.timeout)
end
end
function FullscreenNotification:render()
return Theme.with(function(theme)
local actionButtons = {}
if self.props.actions then
for key, action in self.props.actions do
actionButtons[key] = e(TextButton, {
text = action.text,
style = action.style,
onClick = function()
self:dismiss()
if action.onClick then
local success, err = pcall(action.onClick, self)
if not success then
Log.warn("Error in notification action: " .. tostring(err))
end
end
end,
layoutOrder = -action.layoutOrder,
transparency = self.transparency,
})
end
end
return e("Frame", {
BackgroundColor3 = theme.BackgroundColor,
Size = UDim2.fromScale(1, 1),
ZIndex = self.props.layoutOrder,
}, {
Padding = e("UIPadding", {
PaddingLeft = UDim.new(0, 17),
PaddingRight = UDim.new(0, 15),
PaddingTop = UDim.new(0, 10),
PaddingBottom = UDim.new(0, 10),
}),
Layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
FillDirection = Enum.FillDirection.Vertical,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 10),
}),
Logo = e("ImageLabel", {
ImageTransparency = self.transparency,
Image = Assets.Images.Logo,
ImageColor3 = theme.Header.LogoColor,
BackgroundTransparency = 1,
Size = UDim2.fromOffset(60, 27),
LayoutOrder = 1,
}),
Info = e("TextLabel", {
Text = self.props.text,
FontFace = theme.Font.Main,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Notification.InfoColor,
TextTransparency = self.transparency,
TextXAlignment = Enum.TextXAlignment.Center,
TextYAlignment = Enum.TextYAlignment.Center,
TextWrapped = true,
BackgroundTransparency = 1,
AutomaticSize = Enum.AutomaticSize.Y,
Size = UDim2.fromScale(0.4, 0),
LayoutOrder = 2,
}),
Actions = if self.props.actions
then e("Frame", {
Size = UDim2.new(1, -40, 0, 37),
BackgroundTransparency = 1,
LayoutOrder = 3,
}, {
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
SortOrder = Enum.SortOrder.LayoutOrder,
Padding = UDim.new(0, 5),
}),
Buttons = Roact.createFragment(actionButtons),
})
else nil,
})
end)
end
return FullscreenNotification

View File

@@ -1,66 +0,0 @@
local Rojo = script:FindFirstAncestor("Rojo")
local Packages = Rojo.Packages
local Roact = require(Packages.Roact)
local e = Roact.createElement
local Notification = require(script.Notification)
local FullscreenNotification = require(script.FullscreenNotification)
local Notifications = Roact.Component:extend("Notifications")
function Notifications:render()
local popupNotifs = {}
local fullscreenNotifs = {}
for id, notif in self.props.notifications do
local targetTable = if notif.isFullscreen then fullscreenNotifs else popupNotifs
local targetComponent = if notif.isFullscreen then FullscreenNotification else Notification
targetTable["NotifID_" .. id] = e(targetComponent, {
soundPlayer = self.props.soundPlayer,
text = notif.text,
timeout = notif.timeout,
actions = notif.actions,
layoutOrder = id,
onClose = function()
if notif.onClose then
notif.onClose()
end
self.props.onClose(id)
end,
})
end
return e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
Fullscreen = e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
notifs = Roact.createFragment(fullscreenNotifs),
}),
Popups = e("Frame", {
Size = UDim2.fromScale(1, 1),
BackgroundTransparency = 1,
}, {
Layout = e("UIListLayout", {
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Right,
VerticalAlignment = Enum.VerticalAlignment.Bottom,
Padding = UDim.new(0, 5),
}),
Padding = e("UIPadding", {
PaddingTop = UDim.new(0, 5),
PaddingBottom = UDim.new(0, 5),
PaddingLeft = UDim.new(0, 5),
PaddingRight = UDim.new(0, 5),
}),
notifs = Roact.createFragment(popupNotifs),
}),
})
end
return Notifications

View File

@@ -14,123 +14,6 @@ local EMPTY_TABLE = {}
local e = Roact.createElement 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,
FontFace = theme.Font.Main,
TextSize = theme.TextSize.Body,
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") local ChangeList = Roact.Component:extend("ChangeList")
function ChangeList:init() function ChangeList:init()
@@ -153,9 +36,8 @@ function ChangeList:render()
PaddingRight = UDim.new(0, 5), PaddingRight = UDim.new(0, 5),
} }
local headerRow = changes[1]
local headers = e("Frame", { local headers = e("Frame", {
Size = UDim2.new(1, 0, 0, 24), Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = rowTransparency, BackgroundTransparency = rowTransparency,
BackgroundColor3 = theme.Diff.Row, BackgroundColor3 = theme.Diff.Row,
LayoutOrder = 0, LayoutOrder = 0,
@@ -167,36 +49,36 @@ function ChangeList:render()
HorizontalAlignment = Enum.HorizontalAlignment.Left, HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center, VerticalAlignment = Enum.VerticalAlignment.Center,
}), }),
ColumnA = e("TextLabel", { A = e("TextLabel", {
Text = tostring(headerRow[1]), Text = tostring(changes[1][1]),
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Bold, Font = Enum.Font.GothamBold,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = theme.TextColor, TextColor3 = theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd, TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(0.3, 0, 1, 0), Size = UDim2.new(0.3, 0, 1, 0),
LayoutOrder = 1, LayoutOrder = 1,
}), }),
ColumnB = e("TextLabel", { B = e("TextLabel", {
Text = tostring(headerRow[2]), Text = tostring(changes[1][2]),
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Bold, Font = Enum.Font.GothamBold,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = theme.TextColor, TextColor3 = theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd, TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(0.35, 0, 1, 0), Size = UDim2.new(0.35, 0, 1, 0),
LayoutOrder = 2, LayoutOrder = 2,
}), }),
ColumnC = e("TextLabel", { C = e("TextLabel", {
Text = tostring(headerRow[3]), Text = tostring(changes[1][3]),
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Bold, Font = Enum.Font.GothamBold,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = theme.TextColor, TextColor3 = theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd, TextTruncate = Enum.TextTruncate.AtEnd,
@@ -213,8 +95,91 @@ function ChangeList:render()
local metadata = values[4] or EMPTY_TABLE local metadata = values[4] or EMPTY_TABLE
local isWarning = metadata.isWarning local isWarning = metadata.isWarning
-- Special case for .Source updates
-- because we want to display a syntax highlighted diff for better UX
if self.props.showSourceDiff and tostring(values[1]) == "Source" then
rows[row] = e("Frame", {
Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
BackgroundColor3 = theme.Diff.Row,
BorderSizePixel = 0,
LayoutOrder = row,
}, {
Padding = e("UIPadding", pad),
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center,
}),
A = e("TextLabel", {
Text = (if isWarning then "" else "") .. tostring(values[1]),
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
TextColor3 = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(0.3, 0, 1, 0),
LayoutOrder = 1,
}),
Button = e("TextButton", {
Text = "",
Size = UDim2.new(0.7, 0, 1, -4),
LayoutOrder = 2,
BackgroundTransparency = 1,
[Roact.Event.Activated] = function()
if props.showSourceDiff then
props.showSourceDiff(tostring(values[2]), tostring(values[3]))
end
end,
}, {
e(BorderedContainer, {
size = UDim2.new(1, 0, 1, 0),
transparency = self.props.transparency:map(function(t)
return 0.5 + (0.5 * t)
end),
}, {
Layout = e("UIListLayout", {
FillDirection = Enum.FillDirection.Horizontal,
SortOrder = Enum.SortOrder.LayoutOrder,
HorizontalAlignment = Enum.HorizontalAlignment.Center,
VerticalAlignment = Enum.VerticalAlignment.Center,
Padding = UDim.new(0, 5),
}),
Label = e("TextLabel", {
Text = "View Diff",
BackgroundTransparency = 1,
Font = Enum.Font.GothamMedium,
TextSize = 14,
TextColor3 = theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(0, 65, 1, 0),
LayoutOrder = 1,
}),
Icon = e("ImageLabel", {
Image = Assets.Images.Icons.Expand,
ImageColor3 = theme.Settings.Setting.DescriptionColor,
ImageTransparency = self.props.transparency,
Size = UDim2.new(0, 16, 0, 16),
Position = UDim2.new(0.5, 0, 0.5, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
BackgroundTransparency = 1,
LayoutOrder = 2,
}),
}),
}),
})
continue
end
rows[row] = e("Frame", { rows[row] = e("Frame", {
Size = UDim2.new(1, 0, 0, 24), Size = UDim2.new(1, 0, 0, 30),
BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1, BackgroundTransparency = row % 2 ~= 0 and rowTransparency or 1,
BackgroundColor3 = theme.Diff.Row, BackgroundColor3 = theme.Diff.Row,
BorderSizePixel = 0, BorderSizePixel = 0,
@@ -227,25 +192,44 @@ function ChangeList:render()
HorizontalAlignment = Enum.HorizontalAlignment.Left, HorizontalAlignment = Enum.HorizontalAlignment.Left,
VerticalAlignment = Enum.VerticalAlignment.Center, VerticalAlignment = Enum.VerticalAlignment.Center,
}), }),
ColumnA = e("TextLabel", { A = e("TextLabel", {
Text = (if isWarning then "" else "") .. tostring(values[1]), Text = (if isWarning then "" else "") .. tostring(values[1]),
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = if isWarning then theme.Diff.Warning else theme.TextColor, TextColor3 = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd, TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(0.3, 0, 1, 0), Size = UDim2.new(0.3, 0, 1, 0),
LayoutOrder = 1, LayoutOrder = 1,
}), }),
Content = e(RowContent, { B = e(
values = values, "Frame",
metadata = metadata, {
transparency = props.transparency, BackgroundTransparency = 1,
showStringDiff = props.showStringDiff, Size = UDim2.new(0.35, 0, 1, 0),
showTableDiff = props.showTableDiff, LayoutOrder = 2,
}), },
e(DisplayValue, {
value = values[2],
transparency = props.transparency,
textColor = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
})
),
C = e(
"Frame",
{
BackgroundTransparency = 1,
Size = UDim2.new(0.35, 0, 1, 0),
LayoutOrder = 3,
},
e(DisplayValue, {
value = values[3],
transparency = props.transparency,
textColor = if isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
})
),
}) })
end end
@@ -269,8 +253,8 @@ function ChangeList:render()
}, { }, {
Headers = headers, Headers = headers,
Values = e(ScrollingFrame, { Values = e(ScrollingFrame, {
size = UDim2.new(1, 0, 1, -24), size = UDim2.new(1, 0, 1, -30),
position = UDim2.new(0, 0, 0, 24), position = UDim2.new(0, 0, 0, 30),
contentSize = self.contentSize, contentSize = self.contentSize,
transparency = props.transparency, transparency = props.transparency,
}, rows), }, rows),

View File

@@ -30,10 +30,10 @@ local function DisplayValue(props)
}), }),
}), }),
Label = e("TextLabel", { Label = e("TextLabel", {
Text = string.format("%d, %d, %d", props.value.R * 255, props.value.G * 255, props.value.B * 255), Text = string.format("%d,%d,%d", props.value.R * 255, props.value.G * 255, props.value.B * 255),
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = props.textColor, TextColor3 = props.textColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
@@ -90,8 +90,8 @@ local function DisplayValue(props)
return e("TextLabel", { return e("TextLabel", {
Text = textRepresentation, Text = textRepresentation,
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = props.textColor, TextColor3 = props.textColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
@@ -104,16 +104,11 @@ local function DisplayValue(props)
-- Or special text handling tostring for some? -- Or special text handling tostring for some?
-- Will add as needed, let's see what cases arise. -- 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", { return e("TextLabel", {
Text = textRepresentation, Text = string.gsub(tostring(props.value), "%s", " "),
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = props.textColor, TextColor3 = props.textColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,

View File

@@ -1,4 +1,5 @@
local SelectionService = game:GetService("Selection") local SelectionService = game:GetService("Selection")
local StudioService = game:GetService("StudioService")
local Rojo = script:FindFirstAncestor("Rojo") local Rojo = script:FindFirstAncestor("Rojo")
local Plugin = Rojo.Plugin local Plugin = Rojo.Plugin
@@ -14,8 +15,7 @@ local bindingUtil = require(Plugin.App.bindingUtil)
local e = Roact.createElement local e = Roact.createElement
local ChangeList = require(script.Parent.ChangeList) local ChangeList = require(script.Parent.ChangeList)
local Tooltip = require(Plugin.App.Components.Tooltip) local Tooltip = require(script.Parent.Parent.Tooltip)
local ClassIcon = require(Plugin.App.Components.ClassIcon)
local Expansion = Roact.Component:extend("Expansion") local Expansion = Roact.Component:extend("Expansion")
@@ -28,14 +28,13 @@ function Expansion:render()
return e("Frame", { return e("Frame", {
BackgroundTransparency = 1, BackgroundTransparency = 1,
Size = UDim2.new(1, -props.indent, 1, -24), Size = UDim2.new(1, -props.indent, 1, -30),
Position = UDim2.new(0, props.indent, 0, 24), Position = UDim2.new(0, props.indent, 0, 30),
}, { }, {
ChangeList = e(ChangeList, { ChangeList = e(ChangeList, {
changes = props.changeList, changes = props.changeList,
transparency = props.transparency, transparency = props.transparency,
showStringDiff = props.showStringDiff, showSourceDiff = props.showSourceDiff,
showTableDiff = props.showTableDiff,
}), }),
}) })
end end
@@ -44,7 +43,7 @@ local DomLabel = Roact.Component:extend("DomLabel")
function DomLabel:init() function DomLabel:init()
local initHeight = self.props.elementHeight:getValue() local initHeight = self.props.elementHeight:getValue()
self.expanded = initHeight > 24 self.expanded = initHeight > 30
self.motor = Flipper.SingleMotor.new(initHeight) self.motor = Flipper.SingleMotor.new(initHeight)
self.binding = bindingUtil.fromMotor(self.motor) self.binding = bindingUtil.fromMotor(self.motor)
@@ -53,7 +52,7 @@ function DomLabel:init()
renderExpansion = self.expanded, renderExpansion = self.expanded,
}) })
self.motor:onStep(function(value) self.motor:onStep(function(value)
local renderExpansion = value > 24 local renderExpansion = value > 30
self.props.setElementHeight(value) self.props.setElementHeight(value)
if self.props.updateEvent then if self.props.updateEvent then
@@ -81,7 +80,7 @@ function DomLabel:didUpdate(prevProps)
then then
-- Close the expansion when the domlabel is changed to a different thing -- Close the expansion when the domlabel is changed to a different thing
self.expanded = false self.expanded = false
self.motor:setGoal(Flipper.Spring.new(24, { self.motor:setGoal(Flipper.Spring.new(30, {
frequency = 5, frequency = 5,
dampingRatio = 1, dampingRatio = 1,
})) }))
@@ -90,49 +89,17 @@ end
function DomLabel:render() function DomLabel:render()
local props = self.props local props = self.props
local depth = props.depth or 1
return Theme.with(function(theme) return Theme.with(function(theme)
local color = if props.isWarning local iconProps = StudioService:GetClassIcon(props.className)
then theme.Diff.Warning local indent = (props.depth or 0) * 20 + 25
elseif props.patchType then theme.Diff.Background[props.patchType]
else theme.TextColor
local indent = (depth - 1) * 12 + 15
-- Line guides help indent depth remain readable -- Line guides help indent depth remain readable
local lineGuides = {} local lineGuides = {}
for i = 2, depth do for i = 1, props.depth or 0 do
if props.depthsComplete[i] then lineGuides["Line_" .. i] = e("Frame", {
continue Size = UDim2.new(0, 2, 1, 2),
end Position = UDim2.new(0, (20 * i) + 15, 0, -1),
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, BorderSizePixel = 0,
BackgroundTransparency = props.transparency, BackgroundTransparency = props.transparency,
BackgroundColor3 = theme.BorderedContainer.BorderColor, BackgroundColor3 = theme.BorderedContainer.BorderColor,
@@ -141,8 +108,9 @@ function DomLabel:render()
return e("Frame", { return e("Frame", {
ClipsDescendants = true, ClipsDescendants = true,
BackgroundTransparency = if props.elementIndex % 2 == 0 then 0.985 else 1, BackgroundColor3 = if props.patchType then theme.Diff[props.patchType] else nil,
BackgroundColor3 = theme.Diff.Row, BorderSizePixel = 0,
BackgroundTransparency = props.patchType and props.transparency or 1,
Size = self.binding:map(function(expand) Size = self.binding:map(function(expand)
return UDim2.new(1, 0, 0, expand) return UDim2.new(1, 0, 0, expand)
end), end),
@@ -172,8 +140,8 @@ function DomLabel:render()
if props.changeList then if props.changeList then
self.expanded = not self.expanded self.expanded = not self.expanded
local goalHeight = 24 local goalHeight = 30
+ (if self.expanded then math.clamp(#props.changeList * 24, 24, 24 * 6) else 0) + (if self.expanded then math.clamp(#props.changeList * 30, 30, 30 * 6) else 0)
self.motor:setGoal(Flipper.Spring.new(goalHeight, { self.motor:setGoal(Flipper.Spring.new(goalHeight, {
frequency = 5, frequency = 5,
dampingRatio = 1, dampingRatio = 1,
@@ -198,81 +166,46 @@ function DomLabel:render()
indent = indent, indent = indent,
transparency = props.transparency, transparency = props.transparency,
changeList = props.changeList, changeList = props.changeList,
showStringDiff = props.showStringDiff, showSourceDiff = props.showSourceDiff,
showTableDiff = props.showTableDiff,
}) })
else nil, else nil,
DiffIcon = if props.patchType DiffIcon = if props.patchType
then e("ImageLabel", { then e("ImageLabel", {
Image = Assets.Images.Diff[props.patchType], Image = Assets.Images.Diff[props.patchType],
ImageColor3 = color, ImageColor3 = theme.AddressEntry.PlaceholderColor,
ImageTransparency = props.transparency, ImageTransparency = props.transparency,
BackgroundTransparency = 1, BackgroundTransparency = 1,
Size = UDim2.new(0, 14, 0, 14), Size = UDim2.new(0, 20, 0, 20),
Position = UDim2.new(0, 0, 0, 12), Position = UDim2.new(0, 0, 0, 15),
AnchorPoint = Vector2.new(0, 0.5), AnchorPoint = Vector2.new(0, 0.5),
}) })
else nil, else nil,
ClassIcon = e(ClassIcon, { ClassIcon = e("ImageLabel", {
className = props.className, Image = iconProps.Image,
color = color, ImageTransparency = props.transparency,
transparency = props.transparency, ImageRectOffset = iconProps.ImageRectOffset,
size = UDim2.new(0, 16, 0, 16), ImageRectSize = iconProps.ImageRectSize,
position = UDim2.new(0, indent + 2, 0, 12), BackgroundTransparency = 1,
anchorPoint = Vector2.new(0, 0.5), Size = UDim2.new(0, 20, 0, 20),
Position = UDim2.new(0, indent, 0, 15),
AnchorPoint = Vector2.new(0, 0.5),
}), }),
InstanceName = e("TextLabel", { InstanceName = e("TextLabel", {
Text = (if props.isWarning then "" else "") .. props.name, Text = (if props.isWarning then "" else "") .. props.name .. (props.hint and string.format(
' <font color="#%s">%s</font>',
theme.AddressEntry.PlaceholderColor:ToHex(),
props.hint
) or ""),
RichText = true, RichText = true,
BackgroundTransparency = 1, BackgroundTransparency = 1,
FontFace = if props.patchType then theme.Font.Bold else theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Body, TextSize = 14,
TextColor3 = color, TextColor3 = if props.isWarning then theme.Diff.Warning else theme.Settings.Setting.DescriptionColor,
TextXAlignment = Enum.TextXAlignment.Left, TextXAlignment = Enum.TextXAlignment.Left,
TextTransparency = props.transparency, TextTransparency = props.transparency,
TextTruncate = Enum.TextTruncate.AtEnd, TextTruncate = Enum.TextTruncate.AtEnd,
Size = UDim2.new(1, -indent - 50, 0, 24), Size = UDim2.new(1, -indent - 50, 0, 30),
Position = UDim2.new(0, indent + 22, 0, 0), Position = UDim2.new(0, indent + 30, 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,
FontFace = theme.Font.Thin,
TextSize = theme.TextSize.Body,
TextColor3 = theme.SubTextColor,
TextTransparency = props.transparency,
Size = UDim2.new(0, 0, 0, theme.TextSize.Body),
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,
FontFace = theme.Font.Thin,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Diff.Warning,
TextTransparency = props.transparency,
Size = UDim2.new(0, 0, 0, theme.TextSize.Body),
AutomaticSize = Enum.AutomaticSize.X,
LayoutOrder = 6,
})
else nil,
}), }),
LineGuides = e("Folder", nil, lineGuides), LineGuides = e("Folder", nil, lineGuides),
}) })

View File

@@ -8,8 +8,8 @@ local PatchTree = require(Plugin.PatchTree)
local PatchSet = require(Plugin.PatchSet) local PatchSet = require(Plugin.PatchSet)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer) local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local VirtualScroller = require(Plugin.App.Components.VirtualScroller)
local e = Roact.createElement local e = Roact.createElement
@@ -55,60 +55,33 @@ function PatchVisualizer:render()
end end
-- Recusively draw tree -- Recusively draw tree
local scrollElements, elementHeights, elementIndex = {}, {}, 0 local scrollElements, elementHeights = {}, {}
if patchTree then if patchTree then
local elementTotal = patchTree:getCount()
local depthsComplete = {}
local function drawNode(node, depth) local function drawNode(node, depth)
elementIndex += 1 local elementHeight, setElementHeight = Roact.createBinding(30)
table.insert(elementHeights, elementHeight)
local parentNode = patchTree:getNode(node.parentId) table.insert(
local isFinalChild = true scrollElements,
if parentNode then e(DomLabel, {
for _id, sibling in parentNode.children do updateEvent = self.updateEvent,
if type(sibling) == "table" and sibling.name and sibling.name > node.name then elementHeight = elementHeight,
isFinalChild = false setElementHeight = setElementHeight,
break patchType = node.patchType,
end className = node.className,
end isWarning = node.isWarning,
end instance = node.instance,
name = node.name,
local elementHeight, setElementHeight = Roact.createBinding(24) hint = node.hint,
elementHeights[elementIndex] = elementHeight changeList = node.changeList,
scrollElements[elementIndex] = e(DomLabel, { depth = depth,
transparency = self.props.transparency, transparency = self.props.transparency,
showStringDiff = self.props.showStringDiff, showSourceDiff = self.props.showSourceDiff,
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 end
patchTree:forEach(function(node, depth) patchTree:forEach(function(node, depth)
depthsComplete[depth] = false
for i = depth + 1, #depthsComplete do
depthsComplete[i] = nil
end
drawNode(node, depth) drawNode(node, depth)
end) end)
end end
@@ -118,23 +91,21 @@ function PatchVisualizer:render()
transparency = self.props.transparency, transparency = self.props.transparency,
size = self.props.size, size = self.props.size,
position = self.props.position, position = self.props.position,
anchorPoint = self.props.anchorPoint,
layoutOrder = self.props.layoutOrder, layoutOrder = self.props.layoutOrder,
}, { }, {
CleanMerge = e("TextLabel", { CleanMerge = e("TextLabel", {
Visible = #scrollElements == 0, Visible = #scrollElements == 0,
Text = "No changes to sync, project is up to date.", Text = "No changes to sync, project is up to date.",
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Medium, TextSize = 15,
TextColor3 = theme.TextColor, TextColor3 = theme.Settings.Setting.DescriptionColor,
TextWrapped = true, TextWrapped = true,
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 1, BackgroundTransparency = 1,
}), }),
VirtualScroller = e(VirtualScroller, { VirtualScroller = e(VirtualScroller, {
size = UDim2.new(1, 0, 1, -2), size = UDim2.new(1, 0, 1, 0),
position = UDim2.new(0, 0, 0, 2),
transparency = self.props.transparency, transparency = self.props.transparency,
count = #scrollElements, count = #scrollElements,
updateEvent = self.updateEvent.Event, updateEvent = self.updateEvent.Event,

View File

@@ -10,12 +10,6 @@ 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", {
@@ -34,21 +28,16 @@ local function ScrollingFrame(props)
Size = props.size, Size = props.size,
Position = props.position, Position = props.position,
AnchorPoint = props.anchorPoint, AnchorPoint = props.anchorPoint,
CanvasSize = if props.contentSize CanvasSize = props.contentSize:map(function(value)
then props.contentSize:map(function(value) return UDim2.new(
return UDim2.new( 0,
0, if (props.scrollingDirection and props.scrollingDirection ~= Enum.ScrollingDirection.Y)
if (props.scrollingDirection and props.scrollingDirection ~= Enum.ScrollingDirection.Y) then value.X
then value.X else 0,
else 0, 0,
0, value.Y
value.Y )
) end),
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,

View File

@@ -20,7 +20,6 @@ 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,

View File

@@ -1,4 +1,3 @@
--!strict
--[[ --[[
Based on DiffMatchPatch by Neil Fraser. Based on DiffMatchPatch by Neil Fraser.
https://github.com/google/diff-match-patch https://github.com/google/diff-match-patch
@@ -68,187 +67,8 @@ function StringDiff.findDiffs(text1: string, text2: string): Diffs
end end
-- Cleanup the diff -- Cleanup the diff
diffs = StringDiff._cleanupSemantic(diffs)
diffs = StringDiff._reorderAndMerge(diffs) diffs = StringDiff._reorderAndMerge(diffs)
-- Remove any empty diffs
local cursor = 1
while cursor and diffs[cursor] do
if diffs[cursor].value == "" then
table.remove(diffs, cursor)
else
cursor += 1
end
end
return diffs
end
function StringDiff._computeDiff(text1: string, text2: string): Diffs
-- Assumes that the prefix and suffix have already been trimmed off
-- and shortcut returns have been made so these texts must be different
local text1Length, text2Length = #text1, #text2
if text1Length == 0 then
-- It's simply inserting all of text2 into text1
return { { actionType = StringDiff.ActionTypes.Insert, value = text2 } }
end
if text2Length == 0 then
-- It's simply deleting all of text1
return { { actionType = StringDiff.ActionTypes.Delete, value = text1 } }
end
local longText = if text1Length > text2Length then text1 else text2
local shortText = if text1Length > text2Length then text2 else text1
local shortTextLength = #shortText
-- Shortcut if the shorter string exists entirely inside the longer one
local indexOf = if shortTextLength == 0 then nil else string.find(longText, shortText, 1, true)
if indexOf ~= nil then
local diffs = {
{ actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, 1, indexOf - 1) },
{ actionType = StringDiff.ActionTypes.Equal, value = shortText },
{ actionType = StringDiff.ActionTypes.Insert, value = string.sub(longText, indexOf + shortTextLength) },
}
-- Swap insertions for deletions if diff is reversed
if text1Length > text2Length then
diffs[1].actionType, diffs[3].actionType = StringDiff.ActionTypes.Delete, StringDiff.ActionTypes.Delete
end
return diffs
end
if shortTextLength == 1 then
-- Single character string
-- After the previous shortcut, the character can't be an equality
return {
{ actionType = StringDiff.ActionTypes.Delete, value = text1 },
{ actionType = StringDiff.ActionTypes.Insert, value = text2 },
}
end
return StringDiff._bisect(text1, text2)
end
function StringDiff._cleanupSemantic(diffs: Diffs): Diffs
-- Reduce the number of edits by eliminating semantically trivial equalities.
local changes = false
local equalities = {} -- Stack of indices where equalities are found.
local equalitiesLength = 0 -- Keeping our own length var is faster.
local lastEquality: string? = nil
-- Always equal to diffs[equalities[equalitiesLength]].value
local pointer = 1 -- Index of current position.
-- Number of characters that changed prior to the equality.
local length_insertions1 = 0
local length_deletions1 = 0
-- Number of characters that changed after the equality.
local length_insertions2 = 0
local length_deletions2 = 0
while diffs[pointer] do
if diffs[pointer].actionType == StringDiff.ActionTypes.Equal then -- Equality found.
equalitiesLength = equalitiesLength + 1
equalities[equalitiesLength] = pointer
length_insertions1 = length_insertions2
length_deletions1 = length_deletions2
length_insertions2 = 0
length_deletions2 = 0
lastEquality = diffs[pointer].value
else -- An insertion or deletion.
if diffs[pointer].actionType == StringDiff.ActionTypes.Insert then
length_insertions2 = length_insertions2 + #diffs[pointer].value
else
length_deletions2 = length_deletions2 + #diffs[pointer].value
end
-- Eliminate an equality that is smaller or equal to the edits on both
-- sides of it.
if
lastEquality
and (#lastEquality <= math.max(length_insertions1, length_deletions1))
and (#lastEquality <= math.max(length_insertions2, length_deletions2))
then
-- Duplicate record.
table.insert(
diffs,
equalities[equalitiesLength],
{ actionType = StringDiff.ActionTypes.Delete, value = lastEquality }
)
-- Change second copy to insert.
diffs[equalities[equalitiesLength] + 1].actionType = StringDiff.ActionTypes.Insert
-- Throw away the equality we just deleted.
equalitiesLength = equalitiesLength - 1
-- Throw away the previous equality (it needs to be reevaluated).
equalitiesLength = equalitiesLength - 1
pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0
length_insertions1, length_deletions1 = 0, 0 -- Reset the counters.
length_insertions2, length_deletions2 = 0, 0
lastEquality = nil
changes = true
end
end
pointer = pointer + 1
end
-- Normalize the diff.
if changes then
StringDiff._reorderAndMerge(diffs)
end
StringDiff._cleanupSemanticLossless(diffs)
-- Find any overlaps between deletions and insertions.
-- e.g: <del>abcxxx</del><ins>xxxdef</ins>
-- -> <del>abc</del>xxx<ins>def</ins>
-- e.g: <del>xxxabc</del><ins>defxxx</ins>
-- -> <ins>def</ins>xxx<del>abc</del>
-- Only extract an overlap if it is as big as the edit ahead or behind it.
pointer = 2
while diffs[pointer] do
if
diffs[pointer - 1].actionType == StringDiff.ActionTypes.Delete
and diffs[pointer].actionType == StringDiff.ActionTypes.Insert
then
local deletion = diffs[pointer - 1].value
local insertion = diffs[pointer].value
local overlap_length1 = StringDiff._commonOverlap(deletion, insertion)
local overlap_length2 = StringDiff._commonOverlap(insertion, deletion)
if overlap_length1 >= overlap_length2 then
if overlap_length1 >= #deletion / 2 or overlap_length1 >= #insertion / 2 then
-- Overlap found. Insert an equality and trim the surrounding edits.
table.insert(
diffs,
pointer,
{ actionType = StringDiff.ActionTypes.Equal, value = string.sub(insertion, 1, overlap_length1) }
)
diffs[pointer - 1].value = string.sub(deletion, 1, #deletion - overlap_length1)
diffs[pointer + 1].value = string.sub(insertion, overlap_length1 + 1)
pointer = pointer + 1
end
else
if overlap_length2 >= #deletion / 2 or overlap_length2 >= #insertion / 2 then
-- Reverse overlap found.
-- Insert an equality and swap and trim the surrounding edits.
table.insert(
diffs,
pointer,
{ actionType = StringDiff.ActionTypes.Equal, value = string.sub(deletion, 1, overlap_length2) }
)
diffs[pointer - 1] = {
actionType = StringDiff.ActionTypes.Insert,
value = string.sub(insertion, 1, #insertion - overlap_length2),
}
diffs[pointer + 1] = {
actionType = StringDiff.ActionTypes.Delete,
value = string.sub(deletion, overlap_length2 + 1),
}
pointer = pointer + 1
end
end
pointer = pointer + 1
end
pointer = pointer + 1
end
return diffs return diffs
end end
@@ -304,164 +124,51 @@ function StringDiff._sharedSuffix(text1: string, text2: string): number
return pointerMid return pointerMid
end end
function StringDiff._commonOverlap(text1: string, text2: string): number function StringDiff._computeDiff(text1: string, text2: string): Diffs
-- Determine if the suffix of one string is the prefix of another. -- Assumes that the prefix and suffix have already been trimmed off
-- and shortcut returns have been made so these texts must be different
-- Cache the text lengths to prevent multiple calls. local text1Length, text2Length = #text1, #text2
local text1_length = #text1
local text2_length = #text2 if text1Length == 0 then
-- Eliminate the null case. -- It's simply inserting all of text2 into text1
if text1_length == 0 or text2_length == 0 then return { { actionType = StringDiff.ActionTypes.Insert, value = text2 } }
return 0
end
-- Truncate the longer string.
if text1_length > text2_length then
text1 = string.sub(text1, text1_length - text2_length + 1)
elseif text1_length < text2_length then
text2 = string.sub(text2, 1, text1_length)
end
local text_length = math.min(text1_length, text2_length)
-- Quick check for the worst case.
if text1 == text2 then
return text_length
end end
-- Start by looking for a single character match if text2Length == 0 then
-- and increase length until no match is found. -- It's simply deleting all of text1
-- Performance analysis: https://neil.fraser.name/news/2010/11/04/ return { { actionType = StringDiff.ActionTypes.Delete, value = text1 } }
local best = 0 end
local length = 1
while true do local longText = if text1Length > text2Length then text1 else text2
local pattern = string.sub(text1, text_length - length + 1) local shortText = if text1Length > text2Length then text2 else text1
local found = string.find(text2, pattern, 1, true) local shortTextLength = #shortText
if found == nil then
return best -- 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 end
length = length + found - 1 return diffs
if found == 1 or string.sub(text1, text_length - length + 1) == string.sub(text2, 1, length) then
best = length
length = length + 1
end
end
end
function StringDiff._cleanupSemanticScore(one: string, two: string): number
-- Given two strings, compute a score representing whether the internal
-- boundary falls on logical boundaries.
-- Scores range from 6 (best) to 0 (worst).
if (#one == 0) or (#two == 0) then
-- Edges are the best.
return 6
end end
-- Each port of this function behaves slightly differently due to if shortTextLength == 1 then
-- subtle differences in each language's definition of things like -- Single character string
-- 'whitespace'. Since this function's purpose is largely cosmetic, -- After the previous shortcut, the character can't be an equality
-- the choice has been made to use each language's native features return {
-- rather than force total conformity. { actionType = StringDiff.ActionTypes.Delete, value = text1 },
local char1 = string.sub(one, -1) { actionType = StringDiff.ActionTypes.Insert, value = text2 },
local char2 = string.sub(two, 1, 1) }
local nonAlphaNumeric1 = string.match(char1, "%W")
local nonAlphaNumeric2 = string.match(char2, "%W")
local whitespace1 = nonAlphaNumeric1 and string.match(char1, "%s")
local whitespace2 = nonAlphaNumeric2 and string.match(char2, "%s")
local lineBreak1 = whitespace1 and string.match(char1, "%c")
local lineBreak2 = whitespace2 and string.match(char2, "%c")
local blankLine1 = lineBreak1 and string.match(one, "\n\r?\n$")
local blankLine2 = lineBreak2 and string.match(two, "^\r?\n\r?\n")
if blankLine1 or blankLine2 then
-- Five points for blank lines.
return 5
elseif lineBreak1 or lineBreak2 then
-- Four points for line breaks
-- DEVIATION: Prefer to start on a line break instead of end on it
return if lineBreak1 then 4 else 4.5
elseif nonAlphaNumeric1 and not whitespace1 and whitespace2 then
-- Three points for end of sentences.
return 3
elseif whitespace1 or whitespace2 then
-- Two points for whitespace.
return 2
elseif nonAlphaNumeric1 or nonAlphaNumeric2 then
-- One point for non-alphanumeric.
return 1
end end
return 0
end
function StringDiff._cleanupSemanticLossless(diffs: Diffs) return StringDiff._bisect(text1, text2)
-- Look for single edits surrounded on both sides by equalities
-- which can be shifted sideways to align the edit to a word boundary.
-- e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
local pointer = 2
-- Intentionally ignore the first and last element (don't need checking).
while diffs[pointer + 1] do
local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1]
if
(prevDiff.actionType == StringDiff.ActionTypes.Equal)
and (nextDiff.actionType == StringDiff.ActionTypes.Equal)
then
-- This is a single edit surrounded by equalities.
local diff = diffs[pointer]
local equality1 = prevDiff.value
local edit = diff.value
local equality2 = nextDiff.value
-- First, shift the edit as far left as possible.
local commonOffset = StringDiff._sharedSuffix(equality1, edit)
if commonOffset > 0 then
local commonString = string.sub(edit, -commonOffset)
equality1 = string.sub(equality1, 1, -commonOffset - 1)
edit = commonString .. string.sub(edit, 1, -commonOffset - 1)
equality2 = commonString .. equality2
end
-- Second, step character by character right, looking for the best fit.
local bestEquality1 = equality1
local bestEdit = edit
local bestEquality2 = equality2
local bestScore = StringDiff._cleanupSemanticScore(equality1, edit)
+ StringDiff._cleanupSemanticScore(edit, equality2)
while string.byte(edit, 1) == string.byte(equality2, 1) do
equality1 = equality1 .. string.sub(edit, 1, 1)
edit = string.sub(edit, 2) .. string.sub(equality2, 1, 1)
equality2 = string.sub(equality2, 2)
local score = StringDiff._cleanupSemanticScore(equality1, edit)
+ StringDiff._cleanupSemanticScore(edit, equality2)
-- The > (rather than >=) encourages leading rather than trailing whitespace on edits.
-- I just think it looks better for indentation changes to start the line,
-- since then indenting several lines all have aligned diffs at the start
if score > bestScore then
bestScore = score
bestEquality1 = equality1
bestEdit = edit
bestEquality2 = equality2
end
end
if prevDiff.value ~= bestEquality1 then
-- We have an improvement, save it back to the diff.
if #bestEquality1 > 0 then
diffs[pointer - 1].value = bestEquality1
else
table.remove(diffs, pointer - 1)
pointer = pointer - 1
end
diffs[pointer].value = bestEdit
if #bestEquality2 > 0 then
diffs[pointer + 1].value = bestEquality2
else
table.remove(diffs, pointer + 1)
pointer = pointer - 1
end
end
end
pointer = pointer + 1
end
end end
function StringDiff._bisect(text1: string, text2: string): Diffs function StringDiff._bisect(text1: string, text2: string): Diffs

View File

@@ -1,3 +1,5 @@
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 Packages = Rojo.Packages
@@ -5,15 +7,13 @@ local Packages = Rojo.Packages
local Roact = require(Packages.Roact) local Roact = require(Packages.Roact)
local Log = require(Packages.Log) local Log = require(Packages.Log)
local Highlighter = require(Packages.Highlighter) local Highlighter = require(Packages.Highlighter)
Highlighter.matchStudioSettings()
local StringDiff = require(script:FindFirstChild("StringDiff")) local StringDiff = require(script:FindFirstChild("StringDiff"))
local Timer = require(Plugin.Timer)
local Theme = require(Plugin.App.Theme) local Theme = require(Plugin.App.Theme)
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
local CodeLabel = require(Plugin.App.Components.CodeLabel)
local BorderedContainer = require(Plugin.App.Components.BorderedContainer) local BorderedContainer = require(Plugin.App.Components.BorderedContainer)
local VirtualScroller = require(Plugin.App.Components.VirtualScroller) local ScrollingFrame = require(Plugin.App.Components.ScrollingFrame)
local e = Roact.createElement local e = Roact.createElement
@@ -21,29 +21,27 @@ local StringDiffVisualizer = Roact.Component:extend("StringDiffVisualizer")
function StringDiffVisualizer:init() function StringDiffVisualizer:init()
self.scriptBackground, self.setScriptBackground = Roact.createBinding(Color3.fromRGB(0, 0, 0)) self.scriptBackground, self.setScriptBackground = Roact.createBinding(Color3.fromRGB(0, 0, 0))
self.updateEvent = Instance.new("BindableEvent") self.contentSize, self.setContentSize = Roact.createBinding(Vector2.new(0, 0))
self.lineHeight, self.setLineHeight = Roact.createBinding(15)
self.canvasPosition, self.setCanvasPosition = Roact.createBinding(Vector2.zero)
self.windowWidth, self.setWindowWidth = Roact.createBinding(math.huge)
-- Ensure that the script background is up to date with the current theme -- Ensure that the script background is up to date with the current theme
self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function() self.themeChangedConnection = settings().Studio.ThemeChanged:Connect(function()
-- Delay to allow Highlighter to process the theme change first task.defer(function()
task.delay(1 / 20, function() -- Defer to allow Highlighter to process the theme change first
self:updateScriptBackground() self:updateScriptBackground()
self:updateDiffs()
-- Rerender the virtual list elements
self.updateEvent:Fire()
end) end)
end) end)
self:calculateContentSize()
self:updateScriptBackground() self:updateScriptBackground()
self:updateDiffs()
self:setState({
add = {},
remove = {},
})
end end
function StringDiffVisualizer:willUnmount() function StringDiffVisualizer:willUnmount()
self.themeChangedConnection:Disconnect() self.themeChangedConnection:Disconnect()
self.updateEvent:Destroy()
end end
function StringDiffVisualizer:updateScriptBackground() function StringDiffVisualizer:updateScriptBackground()
@@ -54,189 +52,94 @@ function StringDiffVisualizer:updateScriptBackground()
end end
function StringDiffVisualizer:didUpdate(previousProps) function StringDiffVisualizer:didUpdate(previousProps)
if if previousProps.oldText ~= self.props.oldText or previousProps.newText ~= self.props.newText then
previousProps.currentString ~= self.props.currentString self:calculateContentSize()
or previousProps.incomingString ~= self.props.incomingString local add, remove = self:calculateDiffLines()
then self:setState({
self:updateDiffs() add = add,
remove = remove,
})
end end
end end
function StringDiffVisualizer:updateDiffs() function StringDiffVisualizer:calculateContentSize()
Timer.start("StringDiffVisualizer:updateDiffs") local oldText, newText = self.props.oldText, self.props.newText
local currentString, incomingString = self.props.currentString, self.props.incomingString
local oldTextBounds = TextService:GetTextSize(oldText, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
local newTextBounds = TextService:GetTextSize(newText, 16, Enum.Font.RobotoMono, Vector2.new(99999, 99999))
self.setContentSize(
Vector2.new(math.max(oldTextBounds.X, newTextBounds.X), math.max(oldTextBounds.Y, newTextBounds.Y))
)
end
function StringDiffVisualizer:calculateDiffLines()
local oldText, newText = self.props.oldText, self.props.newText
-- Diff the two texts -- Diff the two texts
local startClock = os.clock() local startClock = os.clock()
local diffs = local diffs = StringDiff.findDiffs(oldText, newText)
StringDiff.findDiffs((string.gsub(currentString, "\t", " ")), (string.gsub(incomingString, "\t", " ")))
local stopClock = os.clock() local stopClock = os.clock()
Log.trace( Log.trace(
"Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections", "Diffing {} byte and {} byte strings took {} microseconds and found {} diff sections",
#currentString, #oldText,
#incomingString, #newText,
math.round((stopClock - startClock) * 1000 * 1000), math.round((stopClock - startClock) * 1000 * 1000),
#diffs #diffs
) )
-- Build the rich text lines -- Determine which lines to highlight
local currentRichTextLines = Highlighter.buildRichTextLines({ local add, remove = {}, {}
src = currentString,
})
local incomingRichTextLines = Highlighter.buildRichTextLines({
src = incomingString,
})
local maxLines = math.max(#currentRichTextLines, #incomingRichTextLines) local oldLineNum, newLineNum = 1, 1
-- Find the diff locations
local currentDiffs, incomingDiffs = {}, {}
local firstDiffLineNum = 0
local currentLineNum, incomingLineNum = 1, 1
local currentIdx, incomingIdx = 1, 1
for _, diff in diffs do for _, diff in diffs do
local actionType, text = diff.actionType, diff.value local actionType, text = diff.actionType, diff.value
local lineCount = select(2, string.gsub(text, "\n", "\n")) local lines = select(2, string.gsub(text, "\n", "\n"))
local lines = string.split(text, "\n")
if actionType == StringDiff.ActionTypes.Equal then if actionType == StringDiff.ActionTypes.Equal then
if lineCount > 0 then oldLineNum += lines
-- Jump cursor ahead to last line newLineNum += lines
currentLineNum += lineCount elseif actionType == StringDiff.ActionTypes.Insert then
incomingLineNum += lineCount if lines > 0 then
currentIdx = #lines[#lines] local textLines = string.split(text, "\n")
incomingIdx = #lines[#lines] for i, textLine in textLines do
if string.match(textLine, "%S") then
add[newLineNum + i - 1] = true
end
end
else else
-- Move along this line if string.match(text, "%S") then
currentIdx += #text add[newLineNum] = true
incomingIdx += #text
end
continue
end
if actionType == StringDiff.ActionTypes.Insert then
if firstDiffLineNum == 0 then
firstDiffLineNum = incomingLineNum
end
for i, lineText in lines do
if i > 1 then
-- Move to next line
incomingLineNum += 1
incomingIdx = 0
end end
if not incomingDiffs[incomingLineNum] then
incomingDiffs[incomingLineNum] = {}
end
-- Mark these characters on this line
table.insert(incomingDiffs[incomingLineNum], {
start = incomingIdx,
stop = incomingIdx + #lineText,
})
incomingIdx += #lineText
end end
newLineNum += lines
elseif actionType == StringDiff.ActionTypes.Delete then elseif actionType == StringDiff.ActionTypes.Delete then
if firstDiffLineNum == 0 then if lines > 0 then
firstDiffLineNum = currentLineNum local textLines = string.split(text, "\n")
end for i, textLine in textLines do
if string.match(textLine, "%S") then
for i, lineText in lines do remove[oldLineNum + i - 1] = true
if i > 1 then end
-- Move to next line
currentLineNum += 1
currentIdx = 0
end end
if not currentDiffs[currentLineNum] then else
currentDiffs[currentLineNum] = {} if string.match(text, "%S") then
remove[oldLineNum] = true
end end
-- Mark these characters on this line
table.insert(currentDiffs[currentLineNum], {
start = currentIdx,
stop = currentIdx + #lineText,
})
currentIdx += #lineText
end end
oldLineNum += lines
else else
Log.warn("Unknown diff action: {} {}", actionType, text) Log.warn("Unknown diff action: {} {}", actionType, text)
end end
end end
Timer.stop() return add, remove
self:setState({
maxLines = maxLines,
currentRichTextLines = currentRichTextLines,
incomingRichTextLines = incomingRichTextLines,
currentDiffs = currentDiffs,
incomingDiffs = incomingDiffs,
})
-- Scroll to the first diff line
task.defer(self.setCanvasPosition, Vector2.new(0, math.max(0, (firstDiffLineNum - 4) * 16)))
end end
function StringDiffVisualizer:render() function StringDiffVisualizer:render()
local currentDiffs, incomingDiffs = self.state.currentDiffs, self.state.incomingDiffs local oldText, newText = self.props.oldText, self.props.newText
local currentRichTextLines, incomingRichTextLines =
self.state.currentRichTextLines, self.state.incomingRichTextLines
local maxLines = self.state.maxLines
return Theme.with(function(theme) return Theme.with(function(theme)
self.setLineHeight(theme.TextSize.Code)
-- Calculate the width of the canvas
-- (One line at a time to avoid the char limit of getTextBoundsAsync)
local canvasWidth = 0
for i = 1, maxLines do
local currentLine = currentRichTextLines[i]
if currentLine and string.find(currentLine, "%S") then
local bounds = getTextBoundsAsync(currentLine, theme.Font.Code, theme.TextSize.Code, math.huge, true)
if bounds.X > canvasWidth then
canvasWidth = bounds.X
end
end
local incomingLine = incomingRichTextLines[i]
if incomingLine and string.find(incomingLine, "%S") then
local bounds = getTextBoundsAsync(incomingLine, theme.Font.Code, theme.TextSize.Code, math.huge, true)
if bounds.X > canvasWidth then
canvasWidth = bounds.X
end
end
end
local lineNumberWidth =
getTextBoundsAsync(tostring(maxLines), theme.Font.Code, theme.TextSize.Body, math.huge, true).X
canvasWidth += lineNumberWidth + 12
local removalScrollMarkers = {}
local insertionScrollMarkers = {}
for lineNum in currentDiffs do
table.insert(
removalScrollMarkers,
e("Frame", {
Size = UDim2.fromScale(0.5, 1 / maxLines),
Position = UDim2.fromScale(0, (lineNum - 1) / maxLines),
BorderSizePixel = 0,
BackgroundColor3 = theme.Diff.Background.Remove,
})
)
end
for lineNum in incomingDiffs do
table.insert(
insertionScrollMarkers,
e("Frame", {
Size = UDim2.fromScale(0.5, 1 / maxLines),
Position = UDim2.fromScale(0.5, (lineNum - 1) / maxLines),
BorderSizePixel = 0,
BackgroundColor3 = theme.Diff.Background.Add,
})
)
end
return e(BorderedContainer, { return e(BorderedContainer, {
size = self.props.size, size = self.props.size,
position = self.props.position, position = self.props.position,
@@ -254,196 +157,43 @@ function StringDiffVisualizer:render()
CornerRadius = UDim.new(0, 5), CornerRadius = UDim.new(0, 5),
}), }),
}), }),
Main = e("Frame", { Separator = e("Frame", {
Size = UDim2.new(1, -10, 1, -2), Size = UDim2.new(0, 2, 1, 0),
Position = UDim2.new(0, 2, 0, 2), Position = UDim2.new(0.5, 0, 0, 0),
BackgroundTransparency = 1, AnchorPoint = Vector2.new(0.5, 0),
[Roact.Change.AbsoluteSize] = function(rbx) BorderSizePixel = 0,
self.setWindowWidth(rbx.AbsoluteSize.X * 0.5 - 10) BackgroundColor3 = theme.BorderedContainer.BorderColor,
end, 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,
}, { }, {
Separator = e("Frame", { Source = e(CodeLabel, {
Size = UDim2.new(0, 2, 1, 0), size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(0.5, 0, 0, 0),
AnchorPoint = Vector2.new(0.5, 0),
BorderSizePixel = 0,
BackgroundColor3 = theme.BorderedContainer.BorderColor,
BackgroundTransparency = 0.5,
}),
Current = e(VirtualScroller, {
position = UDim2.new(0, 0, 0, 0), position = UDim2.new(0, 0, 0, 0),
size = UDim2.new(0.5, -1, 1, 0), text = oldText,
transparency = self.props.transparency, lineBackground = theme.Diff.Remove,
count = maxLines, markedLines = self.state.remove,
updateEvent = self.updateEvent.Event,
canvasWidth = canvasWidth,
canvasPosition = self.canvasPosition,
onCanvasPositionChanged = self.setCanvasPosition,
render = function(i)
local lineDiffs = currentDiffs[i]
local diffFrames = table.create(if lineDiffs then #lineDiffs else 0)
-- Show diff markers over the specific changed characters
if lineDiffs then
local charWidth = math.round(theme.TextSize.Code * 0.5)
for diffIdx, diff in lineDiffs do
local start, stop = diff.start, diff.stop
diffFrames[diffIdx] = e("Frame", {
Size = if #lineDiffs == 1
and start == 0
and stop == 0
then UDim2.fromScale(1, 1)
else UDim2.new(
0,
math.max(charWidth * (stop - start), charWidth * 0.4),
1,
0
),
Position = UDim2.fromOffset(charWidth * start, 0),
BackgroundColor3 = theme.Diff.Background.Remove,
BackgroundTransparency = 0.85,
BorderSizePixel = 0,
ZIndex = -1,
})
end
end
return Roact.createFragment({
LineNumber = e("TextLabel", {
Size = UDim2.new(0, lineNumberWidth + 8, 1, 0),
Text = i,
BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 0.9,
BorderSizePixel = 0,
FontFace = theme.Font.Code,
TextSize = theme.TextSize.Body,
TextColor3 = if lineDiffs then theme.Diff.Background.Remove else theme.SubTextColor,
TextXAlignment = Enum.TextXAlignment.Right,
}, {
Padding = e("UIPadding", { PaddingRight = UDim.new(0, 6) }),
}),
Content = e("Frame", {
Size = UDim2.new(1, -(lineNumberWidth + 10), 1, 0),
Position = UDim2.fromScale(1, 0),
AnchorPoint = Vector2.new(1, 0),
BackgroundColor3 = theme.Diff.Background.Remove,
BackgroundTransparency = if lineDiffs then 0.95 else 1,
BorderSizePixel = 0,
}, {
CodeLabel = e("TextLabel", {
Size = UDim2.fromScale(1, 1),
Position = UDim2.fromScale(0, 0),
Text = currentRichTextLines[i] or "",
RichText = true,
BackgroundTransparency = 1,
BorderSizePixel = 0,
FontFace = theme.Font.Code,
TextSize = theme.TextSize.Code,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
TextColor3 = Color3.fromRGB(255, 255, 255),
}),
DiffFrames = Roact.createFragment(diffFrames),
}),
})
end,
getHeightBinding = function()
return self.lineHeight
end,
}),
Incoming = e(VirtualScroller, {
position = UDim2.new(0.5, 1, 0, 0),
size = UDim2.new(0.5, -1, 1, 0),
transparency = self.props.transparency,
count = maxLines,
updateEvent = self.updateEvent.Event,
canvasWidth = canvasWidth,
canvasPosition = self.canvasPosition,
onCanvasPositionChanged = self.setCanvasPosition,
render = function(i)
local lineDiffs = incomingDiffs[i]
local diffFrames = table.create(if lineDiffs then #lineDiffs else 0)
-- Show diff markers over the specific changed characters
if lineDiffs then
local charWidth = math.round(theme.TextSize.Code * 0.5)
for diffIdx, diff in lineDiffs do
local start, stop = diff.start, diff.stop
diffFrames[diffIdx] = e("Frame", {
Size = if #lineDiffs == 1
and start == 0
and stop == 0
then UDim2.fromScale(1, 1)
else UDim2.new(
0,
math.max(charWidth * (stop - start), charWidth * 0.4),
1,
0
),
Position = UDim2.fromOffset(charWidth * start, 0),
BackgroundColor3 = theme.Diff.Background.Add,
BackgroundTransparency = 0.85,
BorderSizePixel = 0,
ZIndex = -1,
})
end
end
return Roact.createFragment({
LineNumber = e("TextLabel", {
Size = UDim2.new(0, lineNumberWidth + 8, 1, 0),
Text = i,
BackgroundColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 0.9,
BorderSizePixel = 0,
FontFace = theme.Font.Code,
TextSize = theme.TextSize.Body,
TextColor3 = if lineDiffs then theme.Diff.Background.Add else theme.SubTextColor,
TextXAlignment = Enum.TextXAlignment.Right,
}, {
Padding = e("UIPadding", { PaddingRight = UDim.new(0, 6) }),
}),
Content = e("Frame", {
Size = UDim2.new(1, -(lineNumberWidth + 10), 1, 0),
Position = UDim2.fromScale(1, 0),
AnchorPoint = Vector2.new(1, 0),
BackgroundColor3 = theme.Diff.Background.Add,
BackgroundTransparency = if lineDiffs then 0.95 else 1,
BorderSizePixel = 0,
}, {
CodeLabel = e("TextLabel", {
Size = UDim2.fromScale(1, 1),
Position = UDim2.fromScale(0, 0),
Text = incomingRichTextLines[i] or "",
RichText = true,
BackgroundColor3 = theme.Diff.Background.Add,
BackgroundTransparency = 1,
BorderSizePixel = 0,
FontFace = theme.Font.Code,
TextSize = theme.TextSize.Code,
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
TextColor3 = Color3.fromRGB(255, 255, 255),
}),
DiffFrames = Roact.createFragment(diffFrames),
}),
})
end,
getHeightBinding = function()
return self.lineHeight
end,
}), }),
}), }),
ScrollMarkers = e("Frame", { New = e(ScrollingFrame, {
Size = self.windowWidth:map(function(windowWidth) position = UDim2.new(0.5, 5, 0, 2),
return UDim2.new(0, 8, 1, -4 - (if canvasWidth > windowWidth then 10 else 0)) size = UDim2.new(0.5, -7, 1, -4),
end), scrollingDirection = Enum.ScrollingDirection.XY,
Position = UDim2.new(1, -2, 0, 2), transparency = self.props.transparency,
AnchorPoint = Vector2.new(1, 0), contentSize = self.contentSize,
BackgroundTransparency = 1,
}, { }, {
insertions = Roact.createFragment(insertionScrollMarkers), Source = e(CodeLabel, {
removals = Roact.createFragment(removalScrollMarkers), size = UDim2.new(1, 0, 1, 0),
position = UDim2.new(0, 0, 0, 0),
text = newText,
lineBackground = theme.Diff.Add,
markedLines = self.state.add,
}),
}), }),
}) })
end) end)

View File

@@ -1,195 +0,0 @@
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 = theme.Diff.Background[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,
FontFace = theme.Font.Bold,
TextSize = theme.TextSize.Body,
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,
FontFace = theme.Font.Bold,
TextSize = theme.TextSize.Body,
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

View File

@@ -1,209 +0,0 @@
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 = theme.Diff.Background[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,
FontFace = theme.Font.Main,
TextSize = theme.TextSize.Body,
TextColor3 = theme.Diff.Text[line.patchType],
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.Diff.Text[line.patchType],
}),
}),
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.Diff.Text[line.patchType],
}),
}),
})
)
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,
FontFace = theme.Font.Bold,
TextSize = theme.TextSize.Body,
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,
FontFace = theme.Font.Bold,
TextSize = theme.TextSize.Body,
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,
FontFace = theme.Font.Bold,
TextSize = theme.TextSize.Body,
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

View File

@@ -1,48 +0,0 @@
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

View File

@@ -1,59 +0,0 @@
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 Assets = require(Plugin.Assets)
local SlicedImage = require(Plugin.App.Components.SlicedImage)
local e = Roact.createElement
return function(props)
return Theme.with(function(theme)
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, theme.TextSize.Medium),
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,
FontFace = theme.Font.Main,
TextSize = theme.TextSize.Small,
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)
end

View File

@@ -1,3 +1,5 @@
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 Packages = Rojo.Packages
@@ -8,7 +10,6 @@ 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)
local bindingUtil = require(Plugin.App.bindingUtil) local bindingUtil = require(Plugin.App.bindingUtil)
local getTextBoundsAsync = require(Plugin.App.getTextBoundsAsync)
local SlicedImage = require(script.Parent.SlicedImage) local SlicedImage = require(script.Parent.SlicedImage)
local TouchRipple = require(script.Parent.TouchRipple) local TouchRipple = require(script.Parent.TouchRipple)
@@ -40,17 +41,18 @@ end
function TextButton:render() function TextButton:render()
return Theme.with(function(theme) return Theme.with(function(theme)
local textBounds = getTextBoundsAsync(self.props.text, theme.Font.Main, theme.TextSize.Large, math.huge) local textSize =
TextService:GetTextSize(self.props.text, 18, Enum.Font.GothamMedium, Vector2.new(math.huge, math.huge))
local style = self.props.style local style = self.props.style
local buttonTheme = theme.Button[style] theme = theme.Button[style]
local bindingHover = bindingUtil.deriveProperty(self.binding, "hover") local bindingHover = bindingUtil.deriveProperty(self.binding, "hover")
local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled") local bindingEnabled = bindingUtil.deriveProperty(self.binding, "enabled")
return e("ImageButton", { return e("ImageButton", {
Size = UDim2.new(0, (theme.TextSize.Body * 2) + textBounds.X, 0, 34), Size = UDim2.new(0, 15 + textSize.X + 15, 0, 34),
Position = self.props.position, Position = self.props.position,
AnchorPoint = self.props.anchorPoint, AnchorPoint = self.props.anchorPoint,
@@ -72,22 +74,18 @@ function TextButton:render()
end, end,
}, { }, {
TouchRipple = e(TouchRipple, { TouchRipple = e(TouchRipple, {
color = buttonTheme.ActionFillColor, color = theme.ActionFillColor,
transparency = self.props.transparency:map(function(value) transparency = self.props.transparency:map(function(value)
return bindingUtil.blendAlpha({ buttonTheme.ActionFillTransparency, value }) return bindingUtil.blendAlpha({ theme.ActionFillTransparency, value })
end), end),
zIndex = 2, zIndex = 2,
}), }),
Text = e("TextLabel", { Text = e("TextLabel", {
Text = self.props.text, Text = self.props.text,
FontFace = theme.Font.Main, Font = Enum.Font.GothamMedium,
TextSize = theme.TextSize.Large, TextSize = 18,
TextColor3 = bindingUtil.mapLerp( TextColor3 = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.TextColor, theme.Disabled.TextColor),
bindingEnabled,
buttonTheme.Enabled.TextColor,
buttonTheme.Disabled.TextColor
),
TextTransparency = self.props.transparency, TextTransparency = self.props.transparency,
Size = UDim2.new(1, 0, 1, 0), Size = UDim2.new(1, 0, 1, 0),
@@ -97,11 +95,7 @@ function TextButton:render()
Border = style == "Bordered" and e(SlicedImage, { Border = style == "Bordered" and e(SlicedImage, {
slice = Assets.Slices.RoundedBorder, slice = Assets.Slices.RoundedBorder,
color = bindingUtil.mapLerp( color = bindingUtil.mapLerp(bindingEnabled, theme.Enabled.BorderColor, theme.Disabled.BorderColor),
bindingEnabled,
buttonTheme.Enabled.BorderColor,
buttonTheme.Disabled.BorderColor
),
transparency = self.props.transparency, transparency = self.props.transparency,
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
@@ -111,18 +105,14 @@ function TextButton:render()
HoverOverlay = e(SlicedImage, { HoverOverlay = e(SlicedImage, {
slice = Assets.Slices.RoundedBackground, slice = Assets.Slices.RoundedBackground,
color = buttonTheme.ActionFillColor, color = theme.ActionFillColor,
transparency = Roact.joinBindings({ transparency = Roact.joinBindings({
hover = bindingHover:map(function(value) hover = bindingHover:map(function(value)
return 1 - value return 1 - value
end), end),
transparency = self.props.transparency, transparency = self.props.transparency,
}):map(function(values) }):map(function(values)
return bindingUtil.blendAlpha({ return bindingUtil.blendAlpha({ theme.ActionFillTransparency, values.hover, values.transparency })
buttonTheme.ActionFillTransparency,
values.hover,
values.transparency,
})
end), end),
size = UDim2.new(1, 0, 1, 0), size = UDim2.new(1, 0, 1, 0),
@@ -134,8 +124,8 @@ function TextButton:render()
slice = Assets.Slices.RoundedBackground, slice = Assets.Slices.RoundedBackground,
color = bindingUtil.mapLerp( color = bindingUtil.mapLerp(
bindingEnabled, bindingEnabled,
buttonTheme.Enabled.BackgroundColor, theme.Enabled.BackgroundColor,
buttonTheme.Disabled.BackgroundColor theme.Disabled.BackgroundColor
), ),
transparency = self.props.transparency, transparency = self.props.transparency,

Some files were not shown because too many files have changed in this diff Show More