Skip to content

Commit f0dabaf

Browse files
committed
feat(quick-open): Go to File (Mod+G)
A palette that finds files and folders under the active pane's directory by name as you type, and focuses the chosen one in the pane. - The walk is breadth-first (new `walk::walk_breadth_first`, sharing the per-entry rules with the depth-first walk; `leave` is not called) so the nearest entries arrive first, and results re-rank live. - Matching is VS Code's in-order subsequence match, scored with nucleo-matcher; name matches rank above path matches, ties go to the shallower and shorter path. Spaces split the query into pieces. - Walk and ranking run next to the VFS (`newt_common::quick_open`, with Local and Remote clients and a hairpin client for the host's own filesystem in remote sessions); only the top 100 cross the RPC boundary. Query changes travel as signals once the run is listening. - `.gitignore` files (in and above the root, up to the repository's top) and `.git/info/exclude` are applied as the walk goes, through a new `Visitor::listed` hook; no git is run. Hidden entries follow the pane's toggle. - The run lives in the modal's own data, so closing or replacing the modal stops it. - New `[quick_open]` settings: gitignore, follow_symlinks, exclude, limit. Drive-bys: a string-list control in the Settings dialog (list settings such as `environment.extra_path` had no working control); `Pane::reveal` replaces two inline copies of navigate-and-focus; the mock VFS marks dot-files hidden; a `go-to-file` docs scene.
1 parent 8296f0b commit f0dabaf

32 files changed

Lines changed: 2362 additions & 147 deletions

‎Cargo.lock‎

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎FEATURE_DUMP.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,16 @@ Each category can be independently toggled on/off in preferences (Hot Paths sect
14861486

14871487
Selecting an entry navigates via `navigate_to_path`, which takes the whole `VfsPath` — `vfs_id` included, so an entry on a mounted VFS lands on that VFS. Removal is keyed by `bookmark_key`, the verbatim `[[bookmark]]` `path` from settings.toml, which is a display path rather than the wire form.
14881488

1489+
### Go to File (Mod+G)
1490+
1491+
A palette that finds files and folders anywhere under the active pane's directory by name, as you type — the file manager's take on VS Code's Ctrl+P. Enter focuses the chosen entry in the pane it was opened from: the pane goes to the entry's folder and puts the cursor on it, a folder as much as a file. Opening it from there is the pane's own Enter, so Mod+G, a few characters, Enter, Enter opens a file through its association.
1492+
1493+
- **Live**: the directory is walked when the palette opens, nearest entries first (breadth-first: everything in a folder before anything in its subfolders), and results appear and re-rank while the walk goes on. A status line under the input counts the entries seen ("Searching… 12,345 items"), says when the walk stopped at the limit, and how many folders couldn't be read. Nothing is cached: each opening walks afresh, and closing the palette stops the walk.
1494+
- **Matching** is VS Code's: an entry matches when the query's characters appear in its path in order, ignoring case — `fobr` finds `foo_bar.txt`. Spaces split the query into pieces that all have to match (`pane tsx`). There is no other query syntax.
1495+
- **Ranking**: a match within the name ranks above one that needs the folders too, so `pane` puts `Pane.tsx` before `pane_helpers/x.ts`, while `mw/pane` still reaches `main_window/Pane.tsx`. Among matches of the same kind, runs of consecutive characters and characters at word starts (after `/`, `_`, `-`, `.`, a case change) score higher; ties go to the shallower entry, then the shorter path. With an empty query the list is simply the nearest entries. The top 100 are shown, with the matched characters in bold.
1496+
- **What's left out**: hidden entries, unless the pane is showing hidden files; `.git` always; names matching `quick_open.exclude`; and, with `quick_open.gitignore` (on by default), whatever `.gitignore` files ignore — those in the folders walked, those in the folders above up to the repository's top, and the repository's `.git/info/exclude`. The user's global git excludes file is not consulted, and no `git` is run: the rules are read and applied as the walk goes, on any filesystem. Symlinked folders are entered only with `quick_open.follow_symlinks`; the walk stays on one filesystem and stops after `quick_open.limit` entries (200,000).
1497+
- **Where it runs**: next to the files — in a remote session on the remote host, so only the top results cross the connection, whatever the size of the tree; this machine's own files, browsed from a remote session, are walked here. It works on any filesystem a pane can show: local, SFTP, S3, archives, disc images.
1498+
14891499
### Bookmark Operations
14901500

14911501
- **Add Bookmark** (Mod+B): Bookmarks the active pane's current directory. Optional custom name (defaults to the directory name). Stored as `[[bookmark]]` in `settings.toml`. The new entry goes to the *top* of the list, and bookmarking an already-bookmarked path moves it there instead of adding a second copy (any duplicates a hand-edited file already had are collapsed at the same time).
@@ -1684,6 +1694,12 @@ system_bookmarks = true # Show GTK bookmarks (Linux)
16841694
mounts = true # Show mounted volumes
16851695
recent_folders = true # Show recently visited directories
16861696

1697+
[quick_open] # Go to File (Mod+G)
1698+
gitignore = true # Leave out what .gitignore files and .git/info/exclude ignore
1699+
follow_symlinks = false # Enter folders reached through symlinks
1700+
exclude = [] # Name globs never shown or descended into, e.g. ["node_modules", "*.pyc"]
1701+
limit = 200000 # Entries looked at before the search stops
1702+
16871703
[editor]
16881704
word_wrap = false # Default word wrap in the text editor (per-file toggle still overrides)
16891705

@@ -1728,6 +1744,7 @@ Four tabs:
17281744
- Enum → dropdown.
17291745
- Number → number input.
17301746
- String → text input.
1747+
- List of strings → a text input per item with a × to remove it, and a dashed row at the end that adds what's typed into it (Enter or leaving the row); an item emptied out is removed.
17311748
- Custom widgets for complex preferences (rendered below the description):
17321749
- **Columns**: Visible/Hidden panels side by side — visible rows carry a drag handle (mouse drag to reorder, arrow keys when focused); checkboxes toggle simple columns; timestamps get a presentation dropdown (Date & time / Date only / Separate columns).
17331750
- **Default Sort**: Dropdown for sort key + ascending checkbox.
@@ -2040,6 +2057,7 @@ Toggle visibility of files starting with `.` (dot files). The `..` parent direct
20402057
| Mod+. | Copy pane path to other pane | Pane focused |
20412058
| Mod+U | Swap panes (whole pane state, history included) | Pane focused |
20422059
| Mod+P | Hot paths | Any |
2060+
| Mod+G | Go to file | Any |
20432061
| Mod+B | Add bookmark | Pane focused |
20442062
| Mod+Shift+L | Select VFS | Pane focused |
20452063

‎THIRD-PARTY-NOTICES.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ OTHER DEALINGS IN THE FONT SOFTWARE.
154154

155155
## Rust crates
156156

157-
732 crates, the superset across every target platform and feature.
157+
736 crates, the superset across every target platform and feature.
158158

159159
adler2 2.0.1 — 0BSD OR MIT OR Apache-2.0
160160
Copyright (C) Jonas Schievink <jonasschievink@gmail.com>
@@ -385,6 +385,10 @@ OTHER DEALINGS IN THE FONT SOFTWARE.
385385
Copyright (c) 2018 Sam Rijs, Alex Crichton and contributors
386386
crossbeam-channel 0.5.17 — MIT OR Apache-2.0
387387
Copyright (c) 2019 The Crossbeam Project Developers
388+
crossbeam-deque 0.8.8 — MIT OR Apache-2.0
389+
Copyright (c) 2019 The Crossbeam Project Developers
390+
crossbeam-epoch 0.9.21 — MIT OR Apache-2.0
391+
Copyright (c) 2019 The Crossbeam Project Developers
388392
crossbeam-utils 0.8.23 — MIT OR Apache-2.0
389393
Copyright (c) 2019 The Crossbeam Project Developers
390394
crunchy 0.2.4 — MIT
@@ -651,6 +655,8 @@ OTHER DEALINGS IN THE FONT SOFTWARE.
651655
Copyright (c) 2013-2025 The rust-url developers
652656
idna_adapter 1.2.2 — Apache-2.0 OR MIT
653657
Copyright (c) The rust-url developers
658+
ignore 0.4.33 — Unlicense OR MIT
659+
Copyright (c) 2015 Andrew Gallant
654660
iluvatar 0.5.0 — MIT OR Apache-2.0
655661
Copyright (c) 2025 iluvatar contributors
656662
image 0.25.10 — MIT OR Apache-2.0
@@ -809,6 +815,7 @@ OTHER DEALINGS IN THE FONT SOFTWARE.
809815
notify 8.2.0 — CC0-1.0
810816
notify-types 2.1.0 — MIT OR Apache-2.0
811817
Copyright (c) 2023 Notify Contributors
818+
nucleo-matcher 0.3.1 — MPL-2.0
812819
num 0.4.3 — MIT OR Apache-2.0
813820
Copyright (c) 2014 The Rust Project Developers
814821
num-bigint 0.4.8 — MIT OR Apache-2.0

‎TODO.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,5 @@ Design: `design_docs/DESIGN_EXCLUSIVE_WRITES.md`.
104104
## Major new features (groom/write design docs first)
105105

106106
- Batch rename (probably with enrichers preview)
107-
- Quick Open (VS Code's Ctrl+P): fuzzy-find files under the current directory, as a lighter alternative to the search VFS — matches stream in live as the walk proceeds, and choosing one jumps to it. Wants a breadth-first mode for the walker (`libs/newt-common/src/vfs/walk.rs` is depth-first only) so near files surface first.
108107
- Compare & synchronize directories
109108
- Custom styling / theming

‎docs/scenes/library/go-to-file.ts‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { behindDialog, docsColumns, PROJECT } from "../demo";
2+
import type { QuickOpenHit } from "../../../src/lib/bindings";
3+
import type { Scene } from "../types";
4+
5+
// Ranked and highlighted as the backend ranks "visit" over the demo tree.
6+
const hit = (
7+
rel: string,
8+
highlights: number[],
9+
is_dir = false,
10+
): QuickOpenHit => ({
11+
rel,
12+
is_dir,
13+
highlights,
14+
});
15+
const results = [
16+
hit("src/visitor", [4, 5, 6, 7, 8], true),
17+
hit("docs/visitors.md", [5, 6, 7, 8, 9]),
18+
hit("tests/visitor.rs", [6, 7, 8, 9, 10]),
19+
hit("examples/visit_sizes.rs", [9, 10, 11, 12, 13]),
20+
hit("src/visitor/mod.rs", [4, 5, 6, 7, 8]),
21+
hit("src/visitor/sorted.rs", [4, 5, 6, 7, 8]),
22+
hit("src/visitor/parallel.rs", [4, 5, 6, 7, 8]),
23+
];
24+
25+
const scene: Scene = {
26+
description: "Go to File finding entries under the project by name",
27+
size: { width: 1000, height: 600 },
28+
...docsColumns(1000),
29+
window: "main",
30+
state: behindDialog({
31+
type: "quick_open",
32+
data: {
33+
root: { vfs_id: 0, path: PROJECT },
34+
root_display: PROJECT,
35+
update: {
36+
query: "visit",
37+
results,
38+
matched: results.length,
39+
walked: 1284,
40+
walking: false,
41+
unreadable: 0,
42+
truncated: false,
43+
error: null,
44+
},
45+
},
46+
context: { pane_handle: 0 },
47+
}),
48+
commands: {
49+
quick_open_query: () => null,
50+
},
51+
steps: [{ type: "visit" }],
52+
};
53+
54+
export default scene;

‎libs/newt-agent/src/main.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ async fn run_agent() -> Result<(), Error> {
349349
let dispatcher = FilesystemDispatcher::new(filesystem, outbox.clone())
350350
.chain(ShellServiceDispatcher::new(LocalShellService))
351351
.chain(EnricherDispatcher::new(outbox.clone(), enrichers))
352+
.chain(newt_common::api::QuickOpenDispatcher::new(
353+
outbox.clone(),
354+
Arc::new(newt_common::quick_open::QuickOpen::new(registry.clone())),
355+
))
352356
.chain(TerminalDispatcher::new(
353357
newt_common::terminal::Local::with_shell_integration(shell_integration),
354358
))

‎libs/newt-common/Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ hyper = { version = "1", features = ["http1", "server", "client"] }
5050
clap = { version = "4", features = ["derive", "wrap_help"] }
5151
hyper-util = { version = "0.1", features = ["tokio"] }
5252
http-body-util = "0.1"
53+
nucleo-matcher = { version = "0.3", default-features = false, features = ["unicode-normalization", "unicode-casefold"] }
54+
ignore = "0.4"
5355

5456
[dev-dependencies]
5557
serde_json = "1"

‎libs/newt-common/src/api/mod.rs‎

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ pub const API_SYSTEM_HOT_PATHS: Api = Api(500);
6767
pub const API_START_ENRICHMENT: Api = Api(700);
6868
pub const API_ENRICHMENT_EVENT: Api = Api(701);
6969

70+
// Quick Open — long-lived streaming invoke; updates ride
71+
// API_QUICK_OPEN_UPDATE notifications and query changes come as
72+
// API_QUICK_OPEN_QUERY signals, both correlated by QuickOpenId.
73+
pub const API_QUICK_OPEN: Api = Api(710);
74+
pub const API_QUICK_OPEN_UPDATE: Api = Api(711);
75+
pub const API_QUICK_OPEN_QUERY: Api = Api(712);
76+
7077
// Connect-dialog discovery — runs on the session owner, so pane-scoped
7178
// agent mounts list the targets they would actually reach.
7279
pub const API_DISCOVER_SSH_HOSTS: Api = Api(510);
@@ -670,6 +677,117 @@ impl Dispatcher for EnricherDispatcher {
670677
}
671678
}
672679

680+
/// Serves Quick Open runs; the side of the session that owns the
681+
/// filesystem.
682+
pub struct QuickOpenDispatcher {
683+
quick_open: Arc<crate::quick_open::QuickOpen>,
684+
outbox: Outbox,
685+
/// Each run's query, for the signals that change it.
686+
queries: Mutex<HashMap<crate::quick_open::QuickOpenId, tokio::sync::watch::Sender<String>>>,
687+
}
688+
689+
impl QuickOpenDispatcher {
690+
pub fn new(outbox: Outbox, quick_open: Arc<crate::quick_open::QuickOpen>) -> Self {
691+
Self {
692+
quick_open,
693+
outbox,
694+
queries: Mutex::new(HashMap::new()),
695+
}
696+
}
697+
}
698+
699+
#[async_trait::async_trait]
700+
impl Dispatcher for QuickOpenDispatcher {
701+
async fn invoke(&self, api: Api, req: bytes::Bytes) -> Result<Option<bytes::Bytes>, Error> {
702+
use crate::quick_open::{QuickOpenId, QuickOpenOptions, QuickOpenUpdate};
703+
if api != API_QUICK_OPEN {
704+
return Ok(None);
705+
}
706+
let (id, root, options, query): (QuickOpenId, VfsPath, QuickOpenOptions, String) =
707+
decode(&req[..])?;
708+
let (query_tx, query_rx) = tokio::sync::watch::channel(query);
709+
self.queries.lock().insert(id, query_tx);
710+
struct Guard<'a> {
711+
id: QuickOpenId,
712+
queries: &'a Mutex<HashMap<QuickOpenId, tokio::sync::watch::Sender<String>>>,
713+
}
714+
impl Drop for Guard<'_> {
715+
fn drop(&mut self) {
716+
self.queries.lock().remove(&self.id);
717+
}
718+
}
719+
let _guard = Guard {
720+
id,
721+
queries: &self.queries,
722+
};
723+
724+
let (tx, mut rx) = tokio::sync::mpsc::channel::<QuickOpenUpdate>(4);
725+
let run = self.quick_open.run(root, options, query_rx, tx);
726+
tokio::pin!(run);
727+
let ret = loop {
728+
tokio::select! {
729+
ret = &mut run => break ret,
730+
update = rx.recv() => {
731+
let Some(update) = update else {
732+
break (&mut run).await;
733+
};
734+
self.outbox
735+
.send(Message::Notify(API_QUICK_OPEN_UPDATE, encode(&(id, update))?.into()))
736+
.await
737+
.map_err(|_| Error::connection())?;
738+
}
739+
}
740+
};
741+
Ok(Some(encode(&ret)?.into()))
742+
}
743+
744+
async fn notify(&self, api: Api, req: bytes::Bytes) -> Result<bool, Error> {
745+
if api != API_QUICK_OPEN_QUERY {
746+
return Ok(false);
747+
}
748+
let (id, query): (crate::quick_open::QuickOpenId, String) = decode(&req[..])?;
749+
// A run that has ended may still be sent the last keystrokes.
750+
if let Some(tx) = self.queries.lock().get(&id) {
751+
tx.send_replace(query);
752+
}
753+
Ok(true)
754+
}
755+
}
756+
757+
/// Routes Quick Open updates to the runs waiting for them; the side of
758+
/// the session that asked.
759+
pub struct QuickOpenUpdateDispatcher {
760+
pending: crate::quick_open::PendingQuickOpens,
761+
}
762+
763+
impl QuickOpenUpdateDispatcher {
764+
pub fn new(pending: crate::quick_open::PendingQuickOpens) -> Self {
765+
Self { pending }
766+
}
767+
}
768+
769+
#[async_trait::async_trait]
770+
impl Dispatcher for QuickOpenUpdateDispatcher {
771+
async fn invoke(&self, _api: Api, _req: bytes::Bytes) -> Result<Option<bytes::Bytes>, Error> {
772+
Ok(None)
773+
}
774+
775+
async fn notify(&self, api: Api, req: bytes::Bytes) -> Result<bool, Error> {
776+
if api != API_QUICK_OPEN_UPDATE {
777+
return Ok(false);
778+
}
779+
let (id, update): (
780+
crate::quick_open::QuickOpenId,
781+
crate::quick_open::QuickOpenUpdate,
782+
) = decode(&req[..])?;
783+
let tx = self.pending.lock().get(&id).cloned();
784+
if let Some(tx) = tx {
785+
let _ = tx.send(update).await;
786+
}
787+
Ok(true)
788+
}
789+
}
790+
673791
pub struct VfsMountDispatcher {
674792
vfs_manager: Box<dyn VfsManager>,
675793
}

‎libs/newt-common/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub mod hot_paths;
1111
pub mod locale;
1212
pub mod operation;
1313
pub mod proc;
14+
pub mod quick_open;
1415
pub mod rpc;
1516
pub mod shell;
1617
pub mod shell_control;

0 commit comments

Comments
 (0)