mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 10:16:03 -10:00
feat(app): support the diff command line option (#1811)
Signed-off-by: Inf1mum <[email protected]>
This commit is contained in:
1 parent
b5974ab3ac
commit
aa3323ddc5
5 files changed
+183
-8
No files matched your search
+13
-5
@@ -163,11 +163,19 @@ pub fn run() {
|
||||
builder.mount_events(app);
|
||||
let state = AppState::new(app.handle().clone())?;
|
||||
let windows = WindowsState::default();
|
||||
if let Ok(cwd) = std::env::current_dir()
|
||||
&& let Some(path) = windows::cli_path(&std::env::args().collect::<Vec<_>>(), &cwd)
|
||||
{
|
||||
windows::remember_cli_position(&windows, &std::env::args().collect::<Vec<_>>(), &path);
|
||||
windows.set_launch("main", windows::path_launch(path));
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
match windows::cli_launch(&args, &cwd) {
|
||||
Some(windows::CliLaunch::Path(path)) => {
|
||||
windows::remember_cli_position(&windows, &args, &path);
|
||||
windows.set_launch("main", windows::path_launch(path));
|
||||
}
|
||||
// `openidea diff a.txt b.txt`: the main window opens the pair in the diff viewer.
|
||||
Some(windows::CliLaunch::Diff(diff)) => {
|
||||
windows.set_launch("main", windows::Launch { diff: Some(Box::new(diff)), ..Default::default() });
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
let menu_state = menu::MenuState::default();
|
||||
menu::install(app.handle(), &menu_state, &state.recent.list());
|
||||
|
||||
+114
-2
@@ -57,6 +57,9 @@ pub struct Launch {
|
||||
/// Comparison windows: the revisions to compare (boxed: most launches have none).
|
||||
#[serde(default)]
|
||||
pub comparison: Option<Box<ComparisonLaunch>>,
|
||||
/// `openidea diff a.txt b.txt`: the two files to open in the diff viewer (boxed: most launches have none).
|
||||
#[serde(default)]
|
||||
pub diff: Option<Box<DiffFilesLaunch>>,
|
||||
/// Deployment windows: the server and local selection to compare.
|
||||
#[serde(default)]
|
||||
pub deployment: Option<DeploymentLaunch>,
|
||||
@@ -93,6 +96,13 @@ pub struct ComparisonLaunch {
|
||||
pub repo: Option<ide_core::IpcPath>,
|
||||
}
|
||||
|
||||
/// The two files of a command-line diff (`openidea diff a.txt b.txt`), both sides editable.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||
pub struct DiffFilesLaunch {
|
||||
pub left: ide_core::IpcPath,
|
||||
pub right: ide_core::IpcPath,
|
||||
}
|
||||
|
||||
/// A caret position for a file opened from the command line: 0-based line and column.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -356,6 +366,9 @@ pub fn cycle_target(sorted: &[String], current: &str, forward: bool) -> Option<S
|
||||
/// Flags followed by a value (`--line 12`).
|
||||
const VALUE_FLAGS: [&str; 2] = ["--line", "--column"];
|
||||
|
||||
/// The subcommand opening two files side by side (`openidea diff a.txt b.txt`).
|
||||
const DIFF_ARG: &str = "diff";
|
||||
|
||||
/// The project path from command-line arguments (`openidea /path/to/project`):
|
||||
/// the first argument after the binary that is not a flag or a flag's value, resolved against `cwd`.
|
||||
pub fn cli_path(args: &[String], cwd: &Path) -> Option<PathBuf> {
|
||||
@@ -374,6 +387,50 @@ pub fn cli_path(args: &[String], cwd: &Path) -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
/// What the command line asks to open (arguments after the binary, resolved against `cwd`).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CliLaunch {
|
||||
/// `openidea diff a.txt b.txt`: the two files side by side in the diff viewer.
|
||||
Diff(DiffFilesLaunch),
|
||||
/// A project folder or a file (`openidea /path/to/project`).
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
/// What the command line asks to open; see [`CliLaunch`].
|
||||
pub fn cli_launch(args: &[String], cwd: &Path) -> Option<CliLaunch> {
|
||||
if args.get(1).map(String::as_str) == Some(DIFF_ARG) {
|
||||
return match cli_diff_paths(args, cwd).as_slice() {
|
||||
[left, right, ..] => {
|
||||
Some(CliLaunch::Diff(DiffFilesLaunch { left: left.clone().into(), right: right.clone().into() }))
|
||||
}
|
||||
// One file is no pair: open it like any path.
|
||||
[one] => Some(CliLaunch::Path(one.clone())),
|
||||
// `openidea diff` with no files: like starting without arguments.
|
||||
[] => None,
|
||||
};
|
||||
}
|
||||
cli_path(args, cwd).map(CliLaunch::Path)
|
||||
}
|
||||
|
||||
/// The file paths of a `diff` subcommand (everything after it that is not a flag or a flag's
|
||||
/// value), resolved against `cwd`.
|
||||
fn cli_diff_paths(args: &[String], cwd: &Path) -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
let mut rest = args.iter().skip(2);
|
||||
while let Some(a) = rest.next() {
|
||||
if VALUE_FLAGS.contains(&a.as_str()) {
|
||||
rest.next();
|
||||
continue;
|
||||
}
|
||||
if a.starts_with('-') || a.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let p = PathBuf::from(a);
|
||||
paths.push(if p.is_absolute() { p } else { cwd.join(p) });
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
/// `--line <n>` and `--column <n>` (1-based, also as `--line=<n>`) as a 0-based position;
|
||||
/// without `--line` there is none, without `--column` it is the line's start.
|
||||
pub fn cli_position(args: &[String]) -> Option<(u32, u32)> {
|
||||
@@ -1064,8 +1121,9 @@ pub fn on_second_instance(app: &AppHandle, args: Vec<String>, cwd: String) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
match cli_path(&args, Path::new(&cwd)) {
|
||||
Some(path) => {
|
||||
match cli_launch(&args, Path::new(&cwd)) {
|
||||
Some(CliLaunch::Diff(diff)) => open_diff_files(app, diff),
|
||||
Some(CliLaunch::Path(path)) => {
|
||||
remember_cli_position(&app.state::<WindowsState>(), &args, &path);
|
||||
open_path(app, &path)
|
||||
}
|
||||
@@ -1079,6 +1137,27 @@ pub fn on_second_instance(app: &AppHandle, args: Vec<String>, cwd: String) {
|
||||
}
|
||||
}
|
||||
|
||||
/// `openidea diff a.txt b.txt` while the app is running: hands the pair to the window the user is
|
||||
/// looking at (a project window, never an auxiliary one), else opens a new window for it.
|
||||
fn open_diff_files(app: &AppHandle, diff: DiffFilesLaunch) {
|
||||
let launch = Launch { diff: Some(Box::new(diff)), ..Default::default() };
|
||||
let target = target_window(app)
|
||||
.filter(|window| auxiliary_parent(window.label()).is_none())
|
||||
.or_else(|| project_window_labels(app).into_iter().find_map(|label| app.get_webview_window(&label)));
|
||||
if let Some(window) = target {
|
||||
app.state::<WindowsState>().set_launch(window.label(), launch);
|
||||
let _ = app.emit_to(window.label(), LAUNCH_EVENT, ());
|
||||
focus(&window);
|
||||
} else {
|
||||
let app = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = create_window_for(&app, None, launch, false) {
|
||||
tracing::warn!("cannot open window: {}", e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle(app: &AppHandle, current: &str, forward: bool) {
|
||||
let windows = app.webview_windows();
|
||||
let sorted = project_window_labels(app);
|
||||
@@ -1480,6 +1559,39 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_launch_parses_the_diff_subcommand() {
|
||||
let cwd = Path::new("/work");
|
||||
// Two paths: a diff pair; relative paths resolve against the cwd.
|
||||
assert_eq!(
|
||||
cli_launch(&s(&["openidea", "diff", "a.txt", "b.txt"]), cwd),
|
||||
Some(CliLaunch::Diff(DiffFilesLaunch { left: "/work/a.txt".into(), right: "/work/b.txt".into() }))
|
||||
);
|
||||
// Absolute paths stay absolute; a path with spaces is one argument (the shell unquotes it
|
||||
// before the arguments reach us).
|
||||
assert_eq!(
|
||||
cli_launch(&s(&["openidea", "diff", "/abs/my a.txt", "my b.txt"]), cwd),
|
||||
Some(CliLaunch::Diff(DiffFilesLaunch { left: "/abs/my a.txt".into(), right: "/work/my b.txt".into() }))
|
||||
);
|
||||
// Flags and their values are not file paths.
|
||||
assert_eq!(
|
||||
cli_launch(&s(&["openidea", "diff", "--line", "3", "a.txt", "b.txt"]), cwd),
|
||||
Some(CliLaunch::Diff(DiffFilesLaunch { left: "/work/a.txt".into(), right: "/work/b.txt".into() }))
|
||||
);
|
||||
// One file is no pair: it opens like any path.
|
||||
assert_eq!(
|
||||
cli_launch(&s(&["openidea", "diff", "a.txt"]), cwd),
|
||||
Some(CliLaunch::Path(PathBuf::from("/work/a.txt")))
|
||||
);
|
||||
// No files at all: like starting without arguments.
|
||||
assert_eq!(cli_launch(&s(&["openidea", "diff"]), cwd), None);
|
||||
// Only a leading `diff` is the subcommand; the rest still opens a path.
|
||||
assert_eq!(
|
||||
cli_launch(&s(&["openidea", "proj", "diff", "a.txt"]), cwd),
|
||||
Some(CliLaunch::Path(PathBuf::from("/work/proj")))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_position_is_one_based_on_the_command_line() {
|
||||
assert_eq!(cli_position(&s(&["openidea", "--line", "120", "--column", "8", "f"])), Some((119, 7)));
|
||||
|
||||
@@ -903,6 +903,8 @@ export const commands = {
|
||||
positions?: FilePosition[],
|
||||
/** Comparison windows: the revisions to compare (boxed: most launches have none). */
|
||||
comparison?: ComparisonLaunch | null,
|
||||
/** `openidea diff a.txt b.txt`: the two files to open in the diff viewer (boxed: most launches have none). */
|
||||
diff?: DiffFilesLaunch | null,
|
||||
/** Deployment windows: the server and local selection to compare. */
|
||||
deployment?: DeploymentLaunch | null,
|
||||
} | null>("window_take_launch"),
|
||||
@@ -3077,6 +3079,12 @@ export type Diagnostic = {
|
||||
server?: string | null,
|
||||
};
|
||||
|
||||
/** The two files of a command-line diff (`openidea diff a.txt b.txt`), both sides editable. */
|
||||
export type DiffFilesLaunch = {
|
||||
left: IpcPath,
|
||||
right: IpcPath,
|
||||
};
|
||||
|
||||
export type DirectoryCompareEntry = {
|
||||
path: string,
|
||||
status: DirectoryCompareStatus,
|
||||
@@ -4967,6 +4975,8 @@ export type Launch = {
|
||||
positions?: FilePosition[],
|
||||
/** Comparison windows: the revisions to compare (boxed: most launches have none). */
|
||||
comparison?: ComparisonLaunch | null,
|
||||
/** `openidea diff a.txt b.txt`: the two files to open in the diff viewer (boxed: most launches have none). */
|
||||
diff?: DiffFilesLaunch | null,
|
||||
/** Deployment windows: the server and local selection to compare. */
|
||||
deployment?: DeploymentLaunch | null,
|
||||
};
|
||||
|
||||
@@ -46,6 +46,8 @@ vi.mock("@/platform/project", async (orig) => {
|
||||
const real = await orig<typeof import("@/platform/project")>();
|
||||
return { ...real, openProject: (p: string) => openProject(p) };
|
||||
});
|
||||
const diffs: unknown[] = [];
|
||||
vi.mock("@/features/vcs/diffInput", () => ({ openDiff: (data: unknown) => void diffs.push(data) }));
|
||||
|
||||
const {
|
||||
openProjectInWindow,
|
||||
@@ -88,6 +90,7 @@ beforeEach(() => {
|
||||
notify.mockClear();
|
||||
vi.mocked(isRemoteWindow).mockReturnValue(false);
|
||||
opened.length = 0;
|
||||
diffs.length = 0;
|
||||
activePath = null;
|
||||
useProject.setState({ project: null, lightEdit: false, settings: null } as never);
|
||||
});
|
||||
@@ -302,6 +305,33 @@ describe("launch", () => {
|
||||
await consumeLaunch();
|
||||
expect(opened).toEqual(["/x/notes/a.txt @119:7", "/x/notes/b.txt (focus)"]);
|
||||
});
|
||||
|
||||
it("opens the CLI diff pair in LightEdit without a project (openidea diff a b)", async () => {
|
||||
windowTakeLaunch.mockResolvedValueOnce({ project: null, files: [], diff: { left: "/x/a.txt", right: "/x/b.txt" } });
|
||||
await consumeLaunch();
|
||||
expect(lightEditOpen).toHaveBeenCalledWith("/x/b.txt");
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
path: "/x/b.txt",
|
||||
left: { type: "file", path: "/x/a.txt", label: "/x/a.txt" },
|
||||
right: { type: "worktree", label: "/x/b.txt" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("opens the CLI diff pair in the open project without LightEdit", async () => {
|
||||
useProject.setState(projectState("/p") as never);
|
||||
windowTakeLaunch.mockResolvedValueOnce({ project: null, files: [], diff: { left: "/p/a.txt", right: "/p/b.txt" } });
|
||||
await consumeLaunch();
|
||||
expect(lightEditOpen).not.toHaveBeenCalled();
|
||||
expect(diffs).toEqual([
|
||||
{
|
||||
path: "/p/b.txt",
|
||||
left: { type: "file", path: "/p/a.txt", label: "a.txt" },
|
||||
right: { type: "worktree", label: "b.txt" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Attach", () => {
|
||||
|
||||
@@ -212,7 +212,7 @@ async function pickFiles(title: string): Promise<string[]> {
|
||||
return picked === null ? [] : Array.isArray(picked) ? picked : [picked];
|
||||
}
|
||||
|
||||
/** Takes this window's pending launch (if any) and opens its project and files. */
|
||||
/** Takes this window's pending launch (if any) and opens its project, files or diff pair. */
|
||||
export async function consumeLaunch(): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
const launch = await commands.windowTakeLaunch().catch(() => null);
|
||||
@@ -221,6 +221,7 @@ export async function consumeLaunch(): Promise<void> {
|
||||
if (launch?.project) {
|
||||
if (await openHere(launch.project)) await openInEditor(files, positions);
|
||||
} else if (files.length) await openFilesHere(files, positions);
|
||||
else if (launch?.diff) await openCliDiff(launch.diff);
|
||||
else if (launch?.remote) {
|
||||
const { target, path } = launch.remote;
|
||||
await connectRemote(target, path).catch((e) =>
|
||||
@@ -234,6 +235,20 @@ export async function consumeLaunch(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `openidea diff a.txt b.txt`: the two files side by side in the diff viewer (both editable).
|
||||
* Without a project or LightEdit session the right file opens one first, like a launched file does.
|
||||
*/
|
||||
async function openCliDiff(diff: { left: string; right: string }): Promise<void> {
|
||||
const s = useProject.getState();
|
||||
if (!s.project && !s.lightEdit) await openLightEdit(diff.right).catch(() => {});
|
||||
const [{ openDiff }, { compareFilesData }] = await Promise.all([
|
||||
import("@/features/vcs/diffInput"),
|
||||
import("@/features/vcs/compareData"),
|
||||
]);
|
||||
await openDiff(compareFilesData(diff.left, diff.right));
|
||||
}
|
||||
|
||||
/** Closes the project tabs on `side` of the `arg` tab (default: the current one), keeping that tab. */
|
||||
function closeProjectTabs(side: "others" | "left" | "right"): Action["run"] {
|
||||
return async ({ arg }) => {
|
||||
|
||||
Reference in new issue
Block a user