1191 lines · 41.5 KB · Rust
RawDownload
use anyhow::anyhow;use collections::HashSet;use futures::{    channel::{mpsc, oneshot},    pin_mut, SinkExt, StreamExt,};use gpui::{AppContext, AsyncApp, Context, Entity, Task, WeakEntity};use language::Buffer;use mlua::{ExternalResult, Lua, MultiValue, Table, UserData, UserDataMethods};use parking_lot::Mutex;use project::{search::SearchQuery, Fs, Project, ProjectPath, WorktreeId};use regex::Regex;use std::{    path::{Path, PathBuf},    sync::Arc,};use util::{paths::PathMatcher, ResultExt}; struct ForegroundFn(Box<dyn FnOnce(WeakEntity<ScriptingSession>, AsyncApp) + Send>); pub struct ScriptingSession {    project: Entity<Project>,    scripts: Vec<Script>,    changed_buffers: HashSet<Entity<Buffer>>,    foreground_fns_tx: mpsc::Sender<ForegroundFn>,    _invoke_foreground_fns: Task<()>,} impl ScriptingSession {    pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {        let (foreground_fns_tx, mut foreground_fns_rx) = mpsc::channel(128);        ScriptingSession {            project,            scripts: Vec::new(),            changed_buffers: HashSet::default(),            foreground_fns_tx,            _invoke_foreground_fns: cx.spawn(|this, cx| async move {                while let Some(foreground_fn) = foreground_fns_rx.next().await {                    foreground_fn.0(this.clone(), cx.clone());                }            }),        }    }     pub fn changed_buffers(&self) -> impl ExactSizeIterator<Item = &Entity<Buffer>> {        self.changed_buffers.iter()    }     pub fn run_script(        &mut self,        script_src: String,        cx: &mut Context<Self>,    ) -> (ScriptId, Task<()>) {        let id = ScriptId(self.scripts.len() as u32);         let stdout = Arc::new(Mutex::new(String::new()));         let script = Script {            state: ScriptState::Running {                stdout: stdout.clone(),            },        };        self.scripts.push(script);         let task = self.run_lua(script_src, stdout, cx);         let task = cx.spawn(|session, mut cx| async move {            let result = task.await;             session                .update(&mut cx, |session, _cx| {                    let script = session.get_mut(id);                    let stdout = script.stdout_snapshot();                     script.state = match result {                        Ok(()) => ScriptState::Succeeded { stdout },                        Err(error) => ScriptState::Failed { stdout, error },                    };                })                .log_err();        });         (id, task)    }     fn run_lua(        &mut self,        script: String,        stdout: Arc<Mutex<String>>,        cx: &mut Context<Self>,    ) -> Task<anyhow::Result<()>> {        const SANDBOX_PREAMBLE: &str = include_str!("sandbox_preamble.lua");         // TODO Honor all worktrees instead of the first one        let worktree_info = self            .project            .read(cx)            .visible_worktrees(cx)            .next()            .map(|worktree| {                let worktree = worktree.read(cx);                (worktree.id(), worktree.abs_path())            });         let root_dir = worktree_info.as_ref().map(|(_, root)| root.clone());         let fs = self.project.read(cx).fs().clone();        let foreground_fns_tx = self.foreground_fns_tx.clone();         let task = cx.background_spawn({            let stdout = stdout.clone();             async move {                let lua = Lua::new();                lua.set_memory_limit(2 * 1024 * 1024 * 1024)?; // 2 GB                let globals = lua.globals();                 // Use the project root dir as the script's current working dir.                if let Some(root_dir) = &root_dir {                    if let Some(root_dir) = root_dir.to_str() {                        globals.set("cwd", root_dir)?;                    }                }                 globals.set(                    "sb_print",                    lua.create_function({                        let stdout = stdout.clone();                        move |_, args: MultiValue| Self::print(args, &stdout)                    })?,                )?;                globals.set(                    "search",                    lua.create_async_function({                        let foreground_fns_tx = foreground_fns_tx.clone();                        let fs = fs.clone();                        move |lua, regex| {                            let mut foreground_fns_tx = foreground_fns_tx.clone();                            let fs = fs.clone();                            async move {                                Self::search(&lua, &mut foreground_fns_tx, fs, regex)                                    .await                                    .into_lua_err()                            }                        }                    })?,                )?;                globals.set(                    "outline",                    lua.create_async_function({                        let root_dir = root_dir.clone();                        let foreground_fns_tx = foreground_fns_tx.clone();                        move |_lua, path| {                            let mut foreground_fns_tx = foreground_fns_tx.clone();                            let root_dir = root_dir.clone();                            async move {                                Self::outline(root_dir, &mut foreground_fns_tx, path)                                    .await                                    .into_lua_err()                            }                        }                    })?,                )?;                globals.set(                    "sb_io_open",                    lua.create_async_function({                        let worktree_info = worktree_info.clone();                        let foreground_fns_tx = foreground_fns_tx.clone();                        move |lua, (path_str, mode)| {                            let worktree_info = worktree_info.clone();                            let mut foreground_fns_tx = foreground_fns_tx.clone();                            let fs = fs.clone();                            async move {                                Self::io_open(                                    &lua,                                    worktree_info,                                    &mut foreground_fns_tx,                                    fs,                                    path_str,                                    mode,                                )                                .await                            }                        }                    })?,                )?;                globals.set("user_script", script)?;                 lua.load(SANDBOX_PREAMBLE).exec_async().await?;                 // Drop Lua instance to decrement reference count.                drop(lua);                 anyhow::Ok(())            }        });         task    }     pub fn get(&self, script_id: ScriptId) -> &Script {        &self.scripts[script_id.0 as usize]    }     fn get_mut(&mut self, script_id: ScriptId) -> &mut Script {        &mut self.scripts[script_id.0 as usize]    }     /// Sandboxed print() function in Lua.    fn print(args: MultiValue, stdout: &Mutex<String>) -> mlua::Result<()> {        for (index, arg) in args.into_iter().enumerate() {            // Lua's `print()` prints tab characters between each argument.            if index > 0 {                stdout.lock().push('\t');            }             // If the argument's to_string() fails, have the whole function call fail.            stdout.lock().push_str(&arg.to_string()?);        }        stdout.lock().push('\n');         Ok(())    }     /// Sandboxed io.open() function in Lua.    async fn io_open(        lua: &Lua,        worktree_info: Option<(WorktreeId, Arc<Path>)>,        foreground_tx: &mut mpsc::Sender<ForegroundFn>,        fs: Arc<dyn Fs>,        path_str: String,        mode: Option<String>,    ) -> mlua::Result<(Option<Table>, String)> {        let (worktree_id, root_dir) = worktree_info            .ok_or_else(|| mlua::Error::runtime("cannot open file without a root directory"))?;         let mode = mode.unwrap_or_else(|| "r".to_string());         // Parse the mode string to determine read/write permissions        let read_perm = mode.contains('r');        let write_perm = mode.contains('w') || mode.contains('a') || mode.contains('+');        let append = mode.contains('a');        let truncate = mode.contains('w');         // This will be the Lua value returned from the `open` function.        let file = lua.create_table()?;         // Store file metadata in the file        file.set("__mode", mode.clone())?;        file.set("__read_perm", read_perm)?;        file.set("__write_perm", write_perm)?;         let path = match Self::parse_abs_path_in_root_dir(&root_dir, &path_str) {            Ok(path) => path,            Err(err) => return Ok((None, format!("{err}"))),        };         let project_path = ProjectPath {            worktree_id,            path: Path::new(&path_str).into(),        };         // flush / close method        let flush_fn = {            let project_path = project_path.clone();            let foreground_tx = foreground_tx.clone();            lua.create_async_function(move |_lua, file_userdata: mlua::Table| {                let project_path = project_path.clone();                let mut foreground_tx = foreground_tx.clone();                async move {                    Self::io_file_flush(file_userdata, project_path, &mut foreground_tx).await                }            })?        };        file.set("flush", flush_fn.clone())?;        // We don't really hold files open, so we only need to flush on close        file.set("close", flush_fn)?;         // If it's a directory, give it a custom read() and return early.        if fs.is_dir(&path).await {            return Self::io_file_dir(lua, fs, file, &path).await;        }         let mut file_content = Vec::new();         if !truncate {            // Try to read existing content if we're not truncating            match Self::read_buffer(project_path.clone(), foreground_tx).await {                Ok(content) => file_content = content.into_bytes(),                Err(e) => return Ok((None, format!("Error reading file: {}", e))),            }        }         // If in append mode, position should be at the end        let position = if append { file_content.len() } else { 0 };        file.set("__position", position)?;        file.set(            "__content",            lua.create_userdata(FileContent(Arc::new(Mutex::new(file_content))))?,        )?;         // Create file methods         // read method        let read_fn = lua.create_function(Self::io_file_read)?;        file.set("read", read_fn)?;         // write method        let write_fn = lua.create_function(Self::io_file_write)?;        file.set("write", write_fn)?;         // If we got this far, the file was opened successfully        Ok((Some(file), String::new()))    }     async fn read_buffer(        project_path: ProjectPath,        foreground_tx: &mut mpsc::Sender<ForegroundFn>,    ) -> anyhow::Result<String> {        Self::run_foreground_fn(            "read file from buffer",            foreground_tx,            Box::new(move |session, mut cx| {                session.update(&mut cx, |session, cx| {                    let open_buffer_task = session                        .project                        .update(cx, |project, cx| project.open_buffer(project_path, cx));                     cx.spawn(|_, cx| async move {                        let buffer = open_buffer_task.await?;                         let text = buffer.read_with(&cx, |buffer, _cx| buffer.text())?;                        Ok(text)                    })                })            }),        )        .await??        .await    }     async fn io_file_flush(        file_userdata: mlua::Table,        project_path: ProjectPath,        foreground_tx: &mut mpsc::Sender<ForegroundFn>,    ) -> mlua::Result<bool> {        let write_perm = file_userdata.get::<bool>("__write_perm")?;         if write_perm {            let content = file_userdata.get::<mlua::AnyUserData>("__content")?;            let content_ref = content.borrow::<FileContent>()?;            let text = {                let mut content_vec = content_ref.0.lock();                let content_vec = std::mem::take(&mut *content_vec);                String::from_utf8(content_vec).into_lua_err()?            };             Self::write_to_buffer(project_path, text, foreground_tx)                .await                .into_lua_err()?;        }         Ok(true)    }     async fn write_to_buffer(        project_path: ProjectPath,        text: String,        foreground_tx: &mut mpsc::Sender<ForegroundFn>,    ) -> anyhow::Result<()> {        Self::run_foreground_fn(            "write to buffer",            foreground_tx,            Box::new(move |session, mut cx| {                session.update(&mut cx, |session, cx| {                    let open_buffer_task = session                        .project                        .update(cx, |project, cx| project.open_buffer(project_path, cx));                     cx.spawn(move |session, mut cx| async move {                        let buffer = open_buffer_task.await?;                         let diff = buffer                            .update(&mut cx, |buffer, cx| buffer.diff(text, cx))?                            .await;                         buffer.update(&mut cx, |buffer, cx| {                            buffer.apply_diff(diff, cx);                        })?;                         session                            .update(&mut cx, {                                let buffer = buffer.clone();                                 |session, cx| {                                    session                                        .project                                        .update(cx, |project, cx| project.save_buffer(buffer, cx))                                }                            })?                            .await?;                         // If we saved successfully, mark buffer as changed                        session.update(&mut cx, |session, _cx| {                            session.changed_buffers.insert(buffer);                        })                    })                })            }),        )        .await??        .await    }     async fn io_file_dir(        lua: &Lua,        fs: Arc<dyn Fs>,        file: Table,        path: &Path,    ) -> mlua::Result<(Option<Table>, String)> {        // Create a special directory handle        file.set("__is_directory", true)?;         // Store directory entries        let entries = match fs.read_dir(&path).await {            Ok(entries) => {                let mut entry_names = Vec::new();                 // Process the stream of directory entries                pin_mut!(entries);                while let Some(Ok(entry_result)) = entries.next().await {                    if let Some(file_name) = entry_result.file_name() {                        entry_names.push(file_name.to_string_lossy().into_owned());                    }                }                 entry_names            }            Err(e) => return Ok((None, format!("Error reading directory: {}", e))),        };         // Save the list of entries        file.set("__dir_entries", entries)?;        file.set("__dir_position", 0usize)?;         // Create a directory-specific read function        let read_fn = lua.create_function(|_lua, file_userdata: mlua::Table| {            let position = file_userdata.get::<usize>("__dir_position")?;            let entries = file_userdata.get::<Vec<String>>("__dir_entries")?;             if position >= entries.len() {                return Ok(None); // No more entries            }             let entry = entries[position].clone();            file_userdata.set("__dir_position", position + 1)?;             Ok(Some(entry))        })?;        file.set("read", read_fn)?;         // If we got this far, the directory was opened successfully        return Ok((Some(file), String::new()));    }     fn io_file_read(        lua: &Lua,        (file_userdata, format): (Table, Option<mlua::Value>),    ) -> mlua::Result<Option<mlua::String>> {        let read_perm = file_userdata.get::<bool>("__read_perm")?;        if !read_perm {            return Err(mlua::Error::runtime("File not open for reading"));        }         let content = file_userdata.get::<mlua::AnyUserData>("__content")?;        let position = file_userdata.get::<usize>("__position")?;        let content_ref = content.borrow::<FileContent>()?;        let content = content_ref.0.lock();         if position >= content.len() {            return Ok(None); // EOF        }         let (result, new_position) = match Self::io_file_read_format(format)? {            FileReadFormat::All => {                // Read entire file from current position                let result = content[position..].to_vec();                (Some(result), content.len())            }            FileReadFormat::Line => {                if let Some(next_newline_ix) = content[position..].iter().position(|c| *c == b'\n')                {                    let mut line = content[position..position + next_newline_ix].to_vec();                    if line.ends_with(b"\r") {                        line.pop();                    }                    (Some(line), position + next_newline_ix + 1)                } else if position < content.len() {                    let line = content[position..].to_vec();                    (Some(line), content.len())

Lines 1–500 of 1191.