Skip to content

Commit 7c3eecc

Browse files
authored
Add support for querying file outline in assistant script (#26351)
Release Notes: - N/A
1 parent fff37ab commit 7c3eecc

File tree

5 files changed

+119
-45
lines changed

5 files changed

+119
-45
lines changed

Cargo.lock

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

crates/assistant_scripting/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ anyhow.workspace = true
1717
collections.workspace = true
1818
futures.workspace = true
1919
gpui.workspace = true
20+
log.workspace = true
2021
mlua.workspace = true
2122
parking_lot.workspace = true
2223
project.workspace = true

crates/assistant_scripting/src/sandbox_preamble.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ sandbox.tostring = tostring
1313
sandbox.tonumber = tonumber
1414
sandbox.pairs = pairs
1515
sandbox.ipairs = ipairs
16+
17+
-- Access to custom functions
1618
sandbox.search = search
19+
sandbox.outline = outline
1720

1821
-- Create a sandboxed version of LuaFileIO
1922
local io = {}

crates/assistant_scripting/src/session.rs

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
use anyhow::anyhow;
12
use collections::{HashMap, HashSet};
23
use futures::{
34
channel::{mpsc, oneshot},
45
pin_mut, SinkExt, StreamExt,
56
};
67
use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
7-
use mlua::{Lua, MultiValue, Table, UserData, UserDataMethods};
8+
use mlua::{ExternalResult, Lua, MultiValue, Table, UserData, UserDataMethods};
89
use parking_lot::Mutex;
910
use project::{search::SearchQuery, Fs, Project};
1011
use regex::Regex;
@@ -129,9 +130,29 @@ impl ScriptSession {
129130
"search",
130131
lua.create_async_function({
131132
let foreground_fns_tx = foreground_fns_tx.clone();
132-
let fs = fs.clone();
133133
move |lua, regex| {
134-
Self::search(lua, foreground_fns_tx.clone(), fs.clone(), regex)
134+
let mut foreground_fns_tx = foreground_fns_tx.clone();
135+
let fs = fs.clone();
136+
async move {
137+
Self::search(&lua, &mut foreground_fns_tx, fs, regex)
138+
.await
139+
.into_lua_err()
140+
}
141+
}
142+
})?,
143+
)?;
144+
globals.set(
145+
"outline",
146+
lua.create_async_function({
147+
let root_dir = root_dir.clone();
148+
move |_lua, path| {
149+
let mut foreground_fns_tx = foreground_fns_tx.clone();
150+
let root_dir = root_dir.clone();
151+
async move {
152+
Self::outline(root_dir, &mut foreground_fns_tx, path)
153+
.await
154+
.into_lua_err()
155+
}
135156
}
136157
})?,
137158
)?;
@@ -211,27 +232,9 @@ impl ScriptSession {
211232
file.set("__read_perm", read_perm)?;
212233
file.set("__write_perm", write_perm)?;
213234

214-
// Sandbox the path; it must be within root_dir
215-
let path: PathBuf = {
216-
let rust_path = Path::new(&path_str);
217-
218-
// Get absolute path
219-
if rust_path.is_absolute() {
220-
// Check if path starts with root_dir prefix without resolving symlinks
221-
if !rust_path.starts_with(&root_dir) {
222-
return Ok((
223-
None,
224-
format!(
225-
"Error: Absolute path {} is outside the current working directory",
226-
path_str
227-
),
228-
));
229-
}
230-
rust_path.to_path_buf()
231-
} else {
232-
// Make relative path absolute relative to cwd
233-
root_dir.join(rust_path)
234-
}
235+
let path = match Self::parse_abs_path_in_root_dir(&root_dir, &path_str) {
236+
Ok(path) => path,
237+
Err(err) => return Ok((None, format!("{err}"))),
235238
};
236239

237240
// close method
@@ -567,11 +570,11 @@ impl ScriptSession {
567570
}
568571

569572
async fn search(
570-
lua: Lua,
571-
mut foreground_tx: mpsc::Sender<ForegroundFn>,
573+
lua: &Lua,
574+
foreground_tx: &mut mpsc::Sender<ForegroundFn>,
572575
fs: Arc<dyn Fs>,
573576
regex: String,
574-
) -> mlua::Result<Table> {
577+
) -> anyhow::Result<Table> {
575578
// TODO: Allow specification of these options.
576579
let search_query = SearchQuery::regex(
577580
&regex,
@@ -584,18 +587,17 @@ impl ScriptSession {
584587
);
585588
let search_query = match search_query {
586589
Ok(query) => query,
587-
Err(e) => return Err(mlua::Error::runtime(format!("Invalid search query: {}", e))),
590+
Err(e) => return Err(anyhow!("Invalid search query: {}", e)),
588591
};
589592

590593
// TODO: Should use `search_query.regex`. The tool description should also be updated,
591594
// as it specifies standard regex.
592595
let search_regex = match Regex::new(&regex) {
593596
Ok(re) => re,
594-
Err(e) => return Err(mlua::Error::runtime(format!("Invalid regex: {}", e))),
597+
Err(e) => return Err(anyhow!("Invalid regex: {}", e)),
595598
};
596599

597-
let mut abs_paths_rx =
598-
Self::find_search_candidates(search_query, &mut foreground_tx).await?;
600+
let mut abs_paths_rx = Self::find_search_candidates(search_query, foreground_tx).await?;
599601

600602
let mut search_results: Vec<Table> = Vec::new();
601603
while let Some(path) = abs_paths_rx.next().await {
@@ -643,7 +645,7 @@ impl ScriptSession {
643645
async fn find_search_candidates(
644646
search_query: SearchQuery,
645647
foreground_tx: &mut mpsc::Sender<ForegroundFn>,
646-
) -> mlua::Result<mpsc::UnboundedReceiver<PathBuf>> {
648+
) -> anyhow::Result<mpsc::UnboundedReceiver<PathBuf>> {
647649
Self::run_foreground_fn(
648650
"finding search file candidates",
649651
foreground_tx,
@@ -693,14 +695,62 @@ impl ScriptSession {
693695
})
694696
}),
695697
)
696-
.await
698+
.await?
699+
}
700+
701+
async fn outline(
702+
root_dir: Option<Arc<Path>>,
703+
foreground_tx: &mut mpsc::Sender<ForegroundFn>,
704+
path_str: String,
705+
) -> anyhow::Result<String> {
706+
let root_dir = root_dir
707+
.ok_or_else(|| mlua::Error::runtime("cannot get outline without a root directory"))?;
708+
let path = Self::parse_abs_path_in_root_dir(&root_dir, &path_str)?;
709+
let outline = Self::run_foreground_fn(
710+
"getting code outline",
711+
foreground_tx,
712+
Box::new(move |session, cx| {
713+
cx.spawn(move |mut cx| async move {
714+
// TODO: This will not use file content from `fs_changes`. It will also reflect
715+
// user changes that have not been saved.
716+
let buffer = session
717+
.update(&mut cx, |session, cx| {
718+
session
719+
.project
720+
.update(cx, |project, cx| project.open_local_buffer(&path, cx))
721+
})?
722+
.await?;
723+
buffer.update(&mut cx, |buffer, _cx| {
724+
if let Some(outline) = buffer.snapshot().outline(None) {
725+
Ok(outline)
726+
} else {
727+
Err(anyhow!("No outline for file {path_str}"))
728+
}
729+
})
730+
})
731+
}),
732+
)
733+
.await?
734+
.await??;
735+
736+
Ok(outline
737+
.items
738+
.into_iter()
739+
.map(|item| {
740+
if item.text.contains('\n') {
741+
log::error!("Outline item unexpectedly contains newline");
742+
}
743+
format!("{}{}", " ".repeat(item.depth), item.text)
744+
})
745+
.collect::<Vec<String>>()
746+
.join("\n"))
697747
}
698748

699749
async fn run_foreground_fn<R: Send + 'static>(
700750
description: &str,
701751
foreground_tx: &mut mpsc::Sender<ForegroundFn>,
702-
function: Box<dyn FnOnce(WeakEntity<Self>, AsyncApp) -> anyhow::Result<R> + Send>,
703-
) -> mlua::Result<R> {
752+
function: Box<dyn FnOnce(WeakEntity<Self>, AsyncApp) -> R + Send>,
753+
) -> anyhow::Result<R> {
704754
let (response_tx, response_rx) = oneshot::channel();
705755
let send_result = foreground_tx
706756
.send(ForegroundFn(Box::new(move |this, cx| {
@@ -710,19 +760,34 @@ impl ScriptSession {
710760
match send_result {
711761
Ok(()) => (),
712762
Err(err) => {
713-
return Err(mlua::Error::runtime(format!(
714-
"Internal error while enqueuing work for {description}: {err}"
715-
)))
763+
return Err(anyhow::Error::new(err).context(format!(
764+
"Internal error while enqueuing work for {description}"
765+
)));
716766
}
717767
}
718768
match response_rx.await {
719-
Ok(Ok(result)) => Ok(result),
720-
Ok(Err(err)) => Err(mlua::Error::runtime(format!(
721-
"Error while {description}: {err}"
722-
))),
723-
Err(oneshot::Canceled) => Err(mlua::Error::runtime(format!(
769+
Ok(result) => Ok(result),
770+
Err(oneshot::Canceled) => Err(anyhow!(
724771
"Internal error: response oneshot was canceled while {description}."
725-
))),
772+
)),
773+
}
774+
}
775+
776+
fn parse_abs_path_in_root_dir(root_dir: &Path, path_str: &str) -> anyhow::Result<PathBuf> {
777+
let path = Path::new(&path_str);
778+
if path.is_absolute() {
779+
// Check if path starts with root_dir prefix without resolving symlinks
780+
if path.starts_with(&root_dir) {
781+
Ok(path.to_path_buf())
782+
} else {
783+
Err(anyhow!(
784+
"Error: Absolute path {} is outside the current working directory",
785+
path_str
786+
))
787+
}
788+
} else {
789+
// TODO: Does use of `../` break sandbox - is path canonicalization needed?
790+
Ok(root_dir.join(path))
726791
}
727792
}
728793
}

crates/assistant_scripting/src/system_prompt.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@ tell you what the output was. Note that `io` only has `open`, and then the file
1616
it returns only has the methods read, write, and close - it doesn't have popen
1717
or anything else.
1818

19-
There will be a global called `search` which accepts a regex (it's implemented
19+
There is a function called `search` which accepts a regex (it's implemented
2020
using Rust's regex crate, so use that regex syntax) and runs that regex on the
2121
contents of every file in the code base (aside from gitignored files), then
2222
returns an array of tables with two fields: "path" (the path to the file that
2323
had the matches) and "matches" (an array of strings, with each string being a
2424
match that was found within the file).
2525

26+
There is a function called `outline` which accepts the path to a source file,
27+
and returns a string where each line is a declaration. These lines are indented
28+
with 2 spaces to indicate when a declaration is inside another.
29+
2630
When I send you the script output, do not thank me for running it,
2731
act as if you ran it yourself.
2832

0 commit comments

Comments
 (0)