Skip to content

feat(cli): open command-line files in a chosen view, and create missing ones on save - #1070

Merged
PathGao merged 3 commits into
masterfrom
feat/cli-open-mode
Nov 8, 2026
Merged

PathGao merged 3 commits into
masterfrom
feat/cli-open-mode

Conversation

@PathGao

@PathGao PathGao commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator

What this is

Two command-line additions asked for in #1051 by @mrsilver76, who reads in Markpad but wants a third-party app to open a file straight into the editor.

  • --edit, --split and --preview open the files on that command line in that view, for that launch only. "Open existing files in" stays as it is, so double-clicking a file still opens it the usual way. A file that is already open switches to the requested view. If several flags are given, the last one wins.
  • A file named on the command line that doesn't exist yet opens as an empty tab at that path instead of an error. The first save creates the file. Closing the tab untouched creates nothing, as with code new.md or vim new.md.

Closes #1051

Mechanism

startup_paths already kept a path that doesn't exist, on purpose, and dropped every argument starting with -. The flag never got anywhere, and the missing file failed later, at the read in loadMarkdown.

A launch file can reach a window by several routes: send_markdown_path, file-path, open-files-here after the "Ask me" picker, and the routing to a window that is restoring it. Instead of threading a mode through each of them, command_line_paths records each command-line path with the view it asked for in AppState. The window that ends up opening the file takes that record once (take_command_line_open) in documentSession.openFiles, which every one of those routes already ends in. A file opened from Finder or Explorer has no record, so it opens exactly as before. A later command line naming the same file replaces the earlier record, and a macOS open-document event for that file drops it. That second rule covers a request nobody took, for example when the "Ask me" picker is cancelled. openFiles matches an already-open tab by canonical path, so ./a.md or A.md finds the a.md tab.

For a missing file, the tab records the path it is to create (createsFile). This only counts while the tab is still on that path, so Save As or following a link moves it back under the usual rules. The first save or a successful read clears it. While it holds, the save's "changed on disk" check doesn't read the file. It asks path_is_missing instead: nothing there is expected, and anything there is somebody else's. That covers an empty file someone touched and a file that can't be read, whether from a permission error or a Windows sharing violation. An untouched tab of this kind is left out of the window snapshot, so the next launch doesn't try to read a file that was never written.

One launch can deliver the same path twice (setup emits file-path, and send_markdown_path returns it too). The second delivery has no record left, so openFiles activates a tab that is still waiting to create its file instead of reading it, which would have raised "Error loading file".

Scope

  • Command line option to open multiple files into specific window tag #671's --tag is not here. It changes how launch files are delivered, so it should be reviewed on its own.
  • A view flag only reaches tabs through openFiles. Recent files, links and session restore never see it.
  • On Windows and Linux, opening a file through the OS also arrives as argv. A jump-list or shortcut entry for a file that has since been deleted now opens an empty tab at that path instead of an error, as it does for code. A Finder open on macOS still reports the missing file.
  • createsFile is not persisted or carried across windows. A tab that has text but no file yet, once restored or moved to another window, raises the conflict bar on its first save. "Keep mine" writes the file.
  • On macOS, open -a Markpad --args --edit x.md passes the flag on a cold start only. With the app already running, open activates it without forwarding arguments. Running the binary inside the bundle works in both cases.

Tests

  • window_runtime.rs: which flag wins, that a second launch records the view for each file, that a later plain launch or an open-document event clears it, and that only a path with nothing there counts as missing.
  • documents.save.spec.ts:
    • A missing file opens clean and in the requested view.
    • The first save creates it without a conflict, and a later deletion is a conflict again.
    • A file somebody else creates first is not overwritten, whether it is empty, readable or not.
    • A tab moved off that path by Save As is guarded as usual.
    • A tab that reads the file ends the exemption.
    • An untouched tab of this kind is not snapshotted.
    • A delivered view applies to the new tab only.
    • The same missing file delivered twice gives one tab and no error.
  • tabs.spec.ts, through openFiles:
    • An already-open tab switches view, including under another spelling and when its file is missing.
    • A new tab opens in the view, and a missing file opens empty.
    • Each request is taken once, and the same file from Finder afterwards follows the setting.

Each guard has a test that goes red without it. That holds for the pathIsMissing check in the save, the path match in createsItsFile, the clear on read, the openFileMode fallback, the guard and canonical match in openFiles, the view switch for an open missing file, and the snapshot filter.

Verification

macOS 26 (Apple silicon):

npm run check            # 0 errors, 0 warnings
npm run lint             # clean
npm test                 # 952 pass
npm run test:vitest      # 770 pass
cargo test --locked      # 249 pass
cargo clippy --locked --all-targets -- -D warnings
cargo fmt --check

With a release build under its own bundle id:

  1. A cold start with --edit exists.md opened the file in the editor while the setting said Preview.
  2. With the app running, --edit new.md (no such file) added an empty new.md tab in the editor, with no error and no file on disk.
  3. After typing and pressing Cmd+S, the file was on disk with that text. There was no conflict bar.
  4. With exists.md open in the editor, --preview ./exists.md switched that tab to preview. It did not open a second tab.
  5. A cold start with --edit exists.md new2.md (new2.md missing) opened new2.md as an empty editor tab, with no error. If AppKit also delivered it as an open-document event, which drops a pending request, that did not happen before the request was taken.

Not tried: Windows and Linux, which is where #1051 came from. The "Ask me" picker route was not run either. It is covered by reading the code and by the openFiles tests.

…ng ones on save

`--edit`, `--split` and `--preview` open the files on that command line in
that view for that launch only, overriding "Open existing files in". A file
already open switches to it. A file named on the command line that does not
exist yet opens as an empty tab; the first save creates it, and closing it
untouched creates nothing.

Closes #1051
@PathGao
PathGao merged commit 80a53b9 into master Nov 8, 2026
4 checks passed
@PathGao
PathGao deleted the feat/cli-open-mode branch November 8, 2026 08:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can you launch Markpad directly to the editor?

1 participant