Add filter functionality to PatchVisualizer and ConfirmingPage

- Implemented a case-insensitive wildcard matching function in PatchVisualizer to filter visible nodes based on user input.
- Enhanced the ConfirmingPage to include a TextInput for filtering, allowing users to search for specific nodes in the PatchVisualizer.
- Updated the rendering logic to display appropriate messages based on the filter state, improving user experience and clarity.
This commit is contained in:
Joe 2026-02-02 15:34:08 -06:00
parent ff96d98d79
commit c092bb975c
6 changed files with 295 additions and 30 deletions

42
src/cli/cursor.rs Normal file
View file

@ -0,0 +1,42 @@
use std::path::PathBuf;
use std::process::{Command, Stdio};
use anyhow::Context;
use clap::Parser;
use super::resolve_path;
/// Open the project directory in Cursor IDE.
#[derive(Debug, Parser)]
pub struct CursorCommand {
/// Path to open. Defaults to the current directory.
#[clap(default_value = ".")]
pub path: PathBuf,
}
impl CursorCommand {
pub fn run(self) -> anyhow::Result<()> {
let path = resolve_path(&self.path);
// Use cmd /c to run cursor through the shell (cursor is a shell command, not an exe)
#[cfg(windows)]
Command::new("cmd")
.args(["/c", "cursor", &path.to_string_lossy()])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.context("Failed to launch Cursor IDE. Is 'cursor' in your PATH?")?;
#[cfg(not(windows))]
Command::new("cursor")
.arg(&*path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.context("Failed to launch Cursor IDE. Is 'cursor' in your PATH?")?;
Ok(())
}
}

View file

@ -1,12 +1,14 @@
//! Defines Rojo's CLI through clap types.
mod build;
mod cursor;
mod doc;
mod fmt_project;
mod init;
mod plugin;
mod serve;
mod sourcemap;
mod studio;
mod syncback;
mod upload;
@ -16,12 +18,14 @@ use clap::Parser;
use thiserror::Error;
pub use self::build::BuildCommand;
pub use self::cursor::CursorCommand;
pub use self::doc::DocCommand;
pub use self::fmt_project::FmtProjectCommand;
pub use self::init::{InitCommand, InitKind};
pub use self::plugin::{PluginCommand, PluginSubcommand};
pub use self::serve::ServeCommand;
pub use self::sourcemap::SourcemapCommand;
pub use self::studio::StudioCommand;
pub use self::syncback::SyncbackCommand;
pub use self::upload::UploadCommand;
@ -46,8 +50,10 @@ impl Options {
Subcommand::Upload(subcommand) => subcommand.run(),
Subcommand::Sourcemap(subcommand) => subcommand.run(),
Subcommand::FmtProject(subcommand) => subcommand.run(),
Subcommand::Cursor(subcommand) => subcommand.run(),
Subcommand::Doc(subcommand) => subcommand.run(),
Subcommand::Plugin(subcommand) => subcommand.run(),
Subcommand::Studio(subcommand) => subcommand.run(),
Subcommand::Syncback(subcommand) => subcommand.run(self.global),
}
}
@ -120,8 +126,10 @@ pub enum Subcommand {
Upload(UploadCommand),
Sourcemap(SourcemapCommand),
FmtProject(FmtProjectCommand),
Cursor(CursorCommand),
Doc(DocCommand),
Plugin(PluginCommand),
Studio(StudioCommand),
Syncback(SyncbackCommand),
}

79
src/cli/studio.rs Normal file
View file

@ -0,0 +1,79 @@
use std::path::PathBuf;
use std::process::Command;
use anyhow::Context;
use clap::Parser;
use memofs::Vfs;
use serde::Deserialize;
use crate::project::Project;
use super::resolve_path;
#[derive(Deserialize)]
struct UniverseResponse {
#[serde(rename = "universeId")]
universe_id: u64,
}
/// Open a Rojo project in Roblox Studio.
#[derive(Debug, Parser)]
pub struct StudioCommand {
/// Path to the project. Defaults to the current directory.
#[clap(default_value = ".")]
pub project: PathBuf,
}
impl StudioCommand {
pub fn run(self) -> anyhow::Result<()> {
let vfs = Vfs::new_oneshot();
let base_path = resolve_path(&self.project);
let project = Project::load_fuzzy(&vfs, &base_path)?
.context("A project file is required to run 'rojo studio'")?;
let serve_place_ids = project
.serve_place_ids
.as_ref()
.context("No servePlaceIds in project file. Add servePlaceIds to your project file.")?;
let place_id = serve_place_ids
.iter()
.min()
.copied()
.context("servePlaceIds is empty in project file")?;
let universe_id = get_universe_id(place_id)?;
let url = format!(
"roblox-studio:1+launchmode:edit+task:EditPlace+placeId:{}+universeId:{}",
place_id, universe_id
);
// Use cmd /c start to fully detach the process on Windows
#[cfg(windows)]
Command::new("cmd")
.args(["/c", "start", "", &url])
.spawn()
.context("Failed to launch Roblox Studio")?;
#[cfg(not(windows))]
opener::open(&url).context("Failed to open Roblox Studio")?;
Ok(())
}
}
fn get_universe_id(place_id: u64) -> anyhow::Result<u64> {
let url = format!(
"https://apis.roblox.com/universes/v1/places/{}/universe",
place_id
);
let response: UniverseResponse = reqwest::blocking::get(&url)
.context("Failed to fetch universe ID from Roblox API")?
.json()
.context("Failed to parse universe ID response")?;
Ok(response.universe_id)
}

View file

@ -83,6 +83,9 @@ impl SyncbackCommand {
// Determine if we need to download the input file
let resolved_input = resolve_path(&self.input);
let _temp_file: Option<NamedTempFile>;
// Track if we should delete the input file after successful syncback
// (when using default Project.rbxl and file exists locally)
let delete_input_after_syncback: Option<PathBuf>;
// Logic:
// - If --download=PLACEID: always download that specific place
@ -100,11 +103,18 @@ impl SyncbackCommand {
);
let temp_path = temp.path().to_path_buf();
_temp_file = Some(temp);
delete_input_after_syncback = None;
temp_path
}
None if resolved_input.exists() => {
// No --download flag, input file exists: use it
_temp_file = None;
// If using default input path, mark for deletion after success
delete_input_after_syncback = if self.input.as_os_str() == "Project.rbxl" {
Some(resolved_input.clone().into_owned())
} else {
None
};
resolved_input.into_owned()
}
None => {
@ -123,6 +133,7 @@ impl SyncbackCommand {
);
let temp_path = temp.path().to_path_buf();
_temp_file = Some(temp);
delete_input_after_syncback = None;
temp_path
}
};
@ -251,6 +262,18 @@ impl SyncbackCommand {
// Refresh git index if in a git repository
refresh_git_index(base_path);
// Delete input file if using default Project.rbxl location
if let Some(input_path) = &delete_input_after_syncback {
match std::fs::remove_file(input_path) {
Ok(()) => log::info!("Deleted input file: {}", input_path.display()),
Err(e) => log::warn!(
"Failed to delete input file {}: {}",
input_path.display(),
e
),
}
}
} else {
log::info!(
"Would write {} files/folders and remove {} files/folders.",