Simple directory mentions (#14970)
- Adds simple support for directory mentions in the TUI. - Codex App/VS Code will require minor change to recognize a directory mention as such and change the link behavior. - Directory mentions have a trailing slash to differentiate from extensionless files <img width="972" height="382" alt="image" src="https://github.com/user-attachments/assets/8035b1eb-0978-465b-8d7a-4db2e5feca39" /> <img width="978" height="228" alt="image" src="https://github.com/user-attachments/assets/af22cf0b-dd10-4440-9bee-a09915f6ba52" />
This commit is contained in:
parent
42e932d7bf
commit
10eb3ec7fc
14 changed files with 131 additions and 6 deletions
|
|
@ -1,6 +1,13 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"FuzzyFileSearchMatchType": {
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FuzzyFileSearchResult": {
|
||||
"description": "Superset of [`codex_file_search::FileMatch`]",
|
||||
"properties": {
|
||||
|
|
@ -18,6 +25,9 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"match_type": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchMatchType"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -32,6 +42,7 @@
|
|||
},
|
||||
"required": [
|
||||
"file_name",
|
||||
"match_type",
|
||||
"path",
|
||||
"root",
|
||||
"score"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"FuzzyFileSearchMatchType": {
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FuzzyFileSearchResult": {
|
||||
"description": "Superset of [`codex_file_search::FileMatch`]",
|
||||
"properties": {
|
||||
|
|
@ -18,6 +25,9 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"match_type": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchMatchType"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -32,6 +42,7 @@
|
|||
},
|
||||
"required": [
|
||||
"file_name",
|
||||
"match_type",
|
||||
"path",
|
||||
"root",
|
||||
"score"
|
||||
|
|
|
|||
|
|
@ -964,6 +964,13 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FuzzyFileSearchMatchType": {
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FuzzyFileSearchResult": {
|
||||
"description": "Superset of [`codex_file_search::FileMatch`]",
|
||||
"properties": {
|
||||
|
|
@ -981,6 +988,9 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"match_type": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchMatchType"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -995,6 +1005,7 @@
|
|||
},
|
||||
"required": [
|
||||
"file_name",
|
||||
"match_type",
|
||||
"path",
|
||||
"root",
|
||||
"score"
|
||||
|
|
|
|||
|
|
@ -2034,6 +2034,13 @@
|
|||
"title": "FileChangeRequestApprovalResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"FuzzyFileSearchMatchType": {
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FuzzyFileSearchParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
|
@ -2093,6 +2100,9 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"match_type": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchMatchType"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -2107,6 +2117,7 @@
|
|||
},
|
||||
"required": [
|
||||
"file_name",
|
||||
"match_type",
|
||||
"path",
|
||||
"root",
|
||||
"score"
|
||||
|
|
|
|||
|
|
@ -4327,6 +4327,13 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"FuzzyFileSearchMatchType": {
|
||||
"enum": [
|
||||
"file",
|
||||
"directory"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"FuzzyFileSearchParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
|
|
@ -4370,6 +4377,9 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"match_type": {
|
||||
"$ref": "#/definitions/FuzzyFileSearchMatchType"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -4384,6 +4394,7 @@
|
|||
},
|
||||
"required": [
|
||||
"file_name",
|
||||
"match_type",
|
||||
"path",
|
||||
"root",
|
||||
"score"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FuzzyFileSearchMatchType = "file" | "directory";
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType";
|
||||
|
||||
/**
|
||||
* Superset of [`codex_file_search::FileMatch`]
|
||||
*/
|
||||
export type FuzzyFileSearchResult = { root: string, path: string, file_name: string, score: number, indices: Array<number> | null, };
|
||||
export type FuzzyFileSearchResult = { root: string, path: string, match_type: FuzzyFileSearchMatchType, file_name: string, score: number, indices: Array<number> | null, };
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export type { FileChange } from "./FileChange";
|
|||
export type { ForcedLoginMethod } from "./ForcedLoginMethod";
|
||||
export type { FunctionCallOutputBody } from "./FunctionCallOutputBody";
|
||||
export type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentItem";
|
||||
export type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType";
|
||||
export type { FuzzyFileSearchParams } from "./FuzzyFileSearchParams";
|
||||
export type { FuzzyFileSearchResponse } from "./FuzzyFileSearchResponse";
|
||||
export type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult";
|
||||
|
|
|
|||
|
|
@ -800,11 +800,20 @@ pub struct FuzzyFileSearchParams {
|
|||
pub struct FuzzyFileSearchResult {
|
||||
pub root: String,
|
||||
pub path: String,
|
||||
pub match_type: FuzzyFileSearchMatchType,
|
||||
pub file_name: String,
|
||||
pub score: u32,
|
||||
pub indices: Option<Vec<u32>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
pub enum FuzzyFileSearchMatchType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
pub struct FuzzyFileSearchResponse {
|
||||
pub files: Vec<FuzzyFileSearchResult>,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use std::sync::Mutex;
|
|||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use codex_app_server_protocol::FuzzyFileSearchMatchType;
|
||||
use codex_app_server_protocol::FuzzyFileSearchResult;
|
||||
use codex_app_server_protocol::FuzzyFileSearchSessionCompletedNotification;
|
||||
use codex_app_server_protocol::FuzzyFileSearchSessionUpdatedNotification;
|
||||
|
|
@ -60,6 +61,10 @@ pub(crate) async fn run_fuzzy_file_search(
|
|||
FuzzyFileSearchResult {
|
||||
root: m.root.to_string_lossy().to_string(),
|
||||
path: m.path.to_string_lossy().to_string(),
|
||||
match_type: match m.match_type {
|
||||
file_search::MatchType::File => FuzzyFileSearchMatchType::File,
|
||||
file_search::MatchType::Directory => FuzzyFileSearchMatchType::Directory,
|
||||
},
|
||||
file_name: file_name.to_string_lossy().to_string(),
|
||||
score: m.score,
|
||||
indices: m.indices,
|
||||
|
|
@ -231,6 +236,10 @@ fn collect_files(snapshot: &file_search::FileSearchSnapshot) -> Vec<FuzzyFileSea
|
|||
FuzzyFileSearchResult {
|
||||
root: m.root.to_string_lossy().to_string(),
|
||||
path: m.path.to_string_lossy().to_string(),
|
||||
match_type: match m.match_type {
|
||||
file_search::MatchType::File => FuzzyFileSearchMatchType::File,
|
||||
file_search::MatchType::Directory => FuzzyFileSearchMatchType::Directory,
|
||||
},
|
||||
file_name: file_name.to_string_lossy().to_string(),
|
||||
score: m.score,
|
||||
indices: m.indices.clone(),
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
|
|||
{
|
||||
"root": root_path.clone(),
|
||||
"path": "abexy",
|
||||
"match_type": "file",
|
||||
"file_name": "abexy",
|
||||
"score": 84,
|
||||
"indices": [0, 1, 2],
|
||||
|
|
@ -264,6 +265,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
|
|||
{
|
||||
"root": root_path.clone(),
|
||||
"path": sub_abce_rel,
|
||||
"match_type": "file",
|
||||
"file_name": "abce",
|
||||
"score": expected_score,
|
||||
"indices": [4, 5, 7],
|
||||
|
|
@ -271,6 +273,7 @@ async fn test_fuzzy_file_search_sorts_and_includes_indices() -> Result<()> {
|
|||
{
|
||||
"root": root_path.clone(),
|
||||
"path": "abcde",
|
||||
"match_type": "file",
|
||||
"file_name": "abcde",
|
||||
"score": 71,
|
||||
"indices": [0, 1, 4],
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@ pub use cli::Cli;
|
|||
/// A single match result returned from the search.
|
||||
///
|
||||
/// * `score` – Relevance score returned by `nucleo`.
|
||||
/// * `path` – Path to the matched file (relative to the search directory).
|
||||
/// * `path` – Path to the matched entry (file or directory), relative to the
|
||||
/// search directory.
|
||||
/// * `match_type` – Whether this match is a file or directory.
|
||||
/// * `indices` – Optional list of character indices that matched the query.
|
||||
/// These are only filled when the caller of [`run`] sets
|
||||
/// `options.compute_indices` to `true`. The indices vector follows the
|
||||
|
|
@ -52,11 +54,19 @@ pub use cli::Cli;
|
|||
pub struct FileMatch {
|
||||
pub score: u32,
|
||||
pub path: PathBuf,
|
||||
pub match_type: MatchType,
|
||||
pub root: PathBuf,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub indices: Option<Vec<u32>>, // Sorted & deduplicated when present
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum MatchType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
impl FileMatch {
|
||||
pub fn full_path(&self) -> PathBuf {
|
||||
self.root.join(&self.path)
|
||||
|
|
@ -386,7 +396,7 @@ fn get_file_path<'a>(path: &'a Path, search_directories: &[PathBuf]) -> Option<(
|
|||
rel_path.to_str().map(|p| (root_idx, p))
|
||||
}
|
||||
|
||||
/// Walks the search directories and feeds discovered file paths into `nucleo`
|
||||
/// Walks the search directories and feeds discovered paths into `nucleo`
|
||||
/// via the injector.
|
||||
///
|
||||
/// The walker uses `require_git(true)` to match git's own ignore semantics:
|
||||
|
|
@ -448,9 +458,6 @@ fn walker_worker(
|
|||
Ok(entry) => entry,
|
||||
Err(_) => return ignore::WalkState::Continue,
|
||||
};
|
||||
if entry.file_type().is_some_and(|ft| ft.is_dir()) {
|
||||
return ignore::WalkState::Continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
let Some(full_path) = path.to_str() else {
|
||||
return ignore::WalkState::Continue;
|
||||
|
|
@ -552,9 +559,15 @@ fn matcher_worker(
|
|||
} else {
|
||||
None
|
||||
};
|
||||
let match_type = if Path::new(full_path).is_dir() {
|
||||
MatchType::Directory
|
||||
} else {
|
||||
MatchType::File
|
||||
};
|
||||
Some(FileMatch {
|
||||
score: match_.score,
|
||||
path: PathBuf::from(relative_path),
|
||||
match_type,
|
||||
root: inner.search_directories[root_idx].clone(),
|
||||
indices,
|
||||
})
|
||||
|
|
@ -961,6 +974,33 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_returns_directory_matches_for_query() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::create_dir_all(dir.path().join("docs/guides")).unwrap();
|
||||
fs::write(dir.path().join("docs/guides/intro.md"), "intro").unwrap();
|
||||
fs::write(dir.path().join("docs/readme.md"), "readme").unwrap();
|
||||
|
||||
let results = run(
|
||||
"guides",
|
||||
vec![dir.path().to_path_buf()],
|
||||
FileSearchOptions {
|
||||
limit: NonZero::new(20).unwrap(),
|
||||
exclude: Vec::new(),
|
||||
threads: NonZero::new(2).unwrap(),
|
||||
compute_indices: false,
|
||||
respect_gitignore: true,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.expect("run ok");
|
||||
|
||||
assert!(results.matches.iter().any(|m| {
|
||||
m.path == std::path::Path::new("docs").join("guides")
|
||||
&& m.match_type == MatchType::Directory
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_exits_run() {
|
||||
let dir = create_temp_tree(200);
|
||||
|
|
|
|||
|
|
@ -7253,6 +7253,7 @@ mod tests {
|
|||
vec![FileMatch {
|
||||
score: 1,
|
||||
path: PathBuf::from("src/main.rs"),
|
||||
match_type: codex_file_search::MatchType::File,
|
||||
root: PathBuf::from("/tmp"),
|
||||
indices: None,
|
||||
}],
|
||||
|
|
|
|||
|
|
@ -7268,6 +7268,7 @@ mod tests {
|
|||
vec![FileMatch {
|
||||
score: 1,
|
||||
path: PathBuf::from("src/main.rs"),
|
||||
match_type: codex_file_search::MatchType::File,
|
||||
root: PathBuf::from("/tmp"),
|
||||
indices: None,
|
||||
}],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue