945 lines · 35.5 KB · Rust
RawDownload
use anyhow::anyhow;use collections::{HashMap, HashSet};use futures::{    channel::{mpsc, oneshot},    pin_mut, SinkExt, StreamExt,};use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};use mlua::{ExternalResult, Lua, MultiValue, Table, UserData, UserDataMethods};use parking_lot::Mutex;use project::{search::SearchQuery, Fs, Project};use regex::Regex;use std::{    cell::RefCell,    path::{Path, PathBuf},    sync::Arc,};use util::{paths::PathMatcher, ResultExt}; use crate::{SCRIPT_END_TAG, SCRIPT_START_TAG}; struct ForegroundFn(Box<dyn FnOnce(WeakEntity<ScriptSession>, AsyncApp) + Send>); pub struct ScriptSession {    project: Entity<Project>,    // TODO Remove this    fs_changes: Arc<Mutex<HashMap<PathBuf, Vec<u8>>>>,    foreground_fns_tx: mpsc::Sender<ForegroundFn>,    _invoke_foreground_fns: Task<()>,    scripts: Vec<Script>,} impl ScriptSession {    pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {        let (foreground_fns_tx, mut foreground_fns_rx) = mpsc::channel(128);        ScriptSession {            project,            fs_changes: Arc::new(Mutex::new(HashMap::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());                }            }),            scripts: Vec::new(),        }    }     pub fn new_script(&mut self) -> ScriptId {        let id = ScriptId(self.scripts.len() as u32);        let script = Script {            id,            state: ScriptState::Generating,            source: SharedString::new_static(""),        };        self.scripts.push(script);        id    }     pub fn run_script(        &mut self,        script_id: ScriptId,        script_src: String,        cx: &mut Context<Self>,    ) -> Task<anyhow::Result<()>> {        let script = self.get_mut(script_id);         let stdout = Arc::new(Mutex::new(String::new()));        script.source = script_src.clone().into();        script.state = ScriptState::Running {            stdout: stdout.clone(),        };         let task = self.run_lua(script_src, stdout, cx);         cx.emit(ScriptEvent::Spawned(script_id));         cx.spawn(|session, mut cx| async move {            let result = task.await;             session.update(&mut cx, |session, cx| {                let script = session.get_mut(script_id);                let stdout = script.stdout_snapshot();                 script.state = match result {                    Ok(()) => ScriptState::Succeeded { stdout },                    Err(error) => ScriptState::Failed { stdout, error },                };                 cx.emit(ScriptEvent::Exited(script_id))            })        })    }     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 Remove fs_changes        let fs_changes = self.fs_changes.clone();        // TODO Honor all worktrees instead of the first one        let root_dir = self            .project            .read(cx)            .visible_worktrees(cx)            .next()            .map(|worktree| worktree.read(cx).abs_path());         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();                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();                        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();                        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_function({                        let fs_changes = fs_changes.clone();                        let root_dir = root_dir.clone();                        move |lua, (path_str, mode)| {                            Self::io_open(&lua, &fs_changes, root_dir.as_ref(), path_str, mode)                        }                    })?,                )?;                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.    fn io_open(        lua: &Lua,        fs_changes: &Arc<Mutex<HashMap<PathBuf, Vec<u8>>>>,        root_dir: Option<&Arc<Path>>,        path_str: String,        mode: Option<String>,    ) -> mlua::Result<(Option<Table>, String)> {        let root_dir = root_dir            .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("__path", path_str.clone())?;        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}"))),        };         // close method        let close_fn = {            let fs_changes = fs_changes.clone();            lua.create_function(move |_lua, file_userdata: mlua::Table| {                let write_perm = file_userdata.get::<bool>("__write_perm")?;                let path = file_userdata.get::<String>("__path")?;                 if write_perm {                    // When closing a writable file, record the content                    let content = file_userdata.get::<mlua::AnyUserData>("__content")?;                    let content_ref = content.borrow::<FileContent>()?;                    let content_vec = content_ref.0.borrow();                     // Don't actually write to disk; instead, just update fs_changes.                    let path_buf = PathBuf::from(&path);                    fs_changes                        .lock()                        .insert(path_buf.clone(), content_vec.clone());                }                 Ok(true)            })?        };        file.set("close", close_fn)?;         // If it's a directory, give it a custom read() and return early.        if path.is_dir() {            // TODO handle the case where we changed it in the in-memory fs             // Create a special directory handle            file.set("__is_directory", true)?;             // Store directory entries            let entries = match std::fs::read_dir(&path) {                Ok(entries) => {                    let mut entry_names = Vec::new();                    for entry in entries.flatten() {                        entry_names.push(entry.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()));        }         let fs_changes_map = fs_changes.lock();         let is_in_changes = fs_changes_map.contains_key(&path);        let file_exists = is_in_changes || path.exists();        let mut file_content = Vec::new();         if file_exists && !truncate {            if is_in_changes {                file_content = fs_changes_map.get(&path).unwrap().clone();            } else {                // Try to read existing content if file exists and we're not truncating                match std::fs::read(&path) {                    Ok(content) => file_content = content,                    Err(e) => return Ok((None, format!("Error reading file: {}", e))),                }            }        }         drop(fs_changes_map); // Unlock the fs_changes mutex.         // If in append mode, position should be at the end        let position = if append && file_exists {            file_content.len()        } else {            0        };        file.set("__position", position)?;        file.set(            "__content",            lua.create_userdata(FileContent(RefCell::new(file_content)))?,        )?;         // Create file methods         // read method        let read_fn = {            lua.create_function(                |_lua, (file_userdata, format): (mlua::Table, Option<mlua::Value>)| {                    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 mut position = file_userdata.get::<usize>("__position")?;                    let content_ref = content.borrow::<FileContent>()?;                    let content_vec = content_ref.0.borrow();                     if position >= content_vec.len() {                        return Ok(None); // EOF                    }                     match format {                        Some(mlua::Value::String(s)) => {                            let lossy_string = s.to_string_lossy();                            let format_str: &str = lossy_string.as_ref();                             // Only consider the first 2 bytes, since it's common to pass e.g. "*all"  instead of "*a"                            match &format_str[0..2] {                                "*a" => {                                    // Read entire file from current position                                    let result = String::from_utf8_lossy(&content_vec[position..])                                        .to_string();                                    position = content_vec.len();                                    file_userdata.set("__position", position)?;                                    Ok(Some(result))                                }                                "*l" => {                                    // Read next line                                    let mut line = Vec::new();                                    let mut found_newline = false;                                     while position < content_vec.len() {                                        let byte = content_vec[position];                                        position += 1;                                         if byte == b'\n' {                                            found_newline = true;                                            break;                                        }                                         // Skip \r in \r\n sequence but add it if it's alone                                        if byte == b'\r' {                                            if position < content_vec.len()                                                && content_vec[position] == b'\n'                                            {                                                position += 1;                                                found_newline = true;                                                break;                                            }                                        }                                         line.push(byte);                                    }                                     file_userdata.set("__position", position)?;                                     if !found_newline                                        && line.is_empty()                                        && position >= content_vec.len()                                    {                                        return Ok(None); // EOF                                    }                                     let result = String::from_utf8_lossy(&line).to_string();                                    Ok(Some(result))                                }                                "*n" => {                                    // Try to parse as a number (number of bytes to read)                                    match format_str.parse::<usize>() {                                        Ok(n) => {                                            let end =                                                std::cmp::min(position + n, content_vec.len());                                            let bytes = &content_vec[position..end];                                            let result = String::from_utf8_lossy(bytes).to_string();                                            position = end;                                            file_userdata.set("__position", position)?;                                            Ok(Some(result))                                        }                                        Err(_) => Err(mlua::Error::runtime(format!(                                            "Invalid format: {}",                                            format_str                                        ))),                                    }                                }                                "*L" => {                                    // Read next line keeping the end of line                                    let mut line = Vec::new();                                     while position < content_vec.len() {                                        let byte = content_vec[position];                                        position += 1;                                         line.push(byte);                                         if byte == b'\n' {                                            break;                                        }                                         // If we encounter a \r, add it and check if the next is \n                                        if byte == b'\r'                                            && position < content_vec.len()                                            && content_vec[position] == b'\n'                                        {                                            line.push(content_vec[position]);                                            position += 1;                                            break;                                        }                                    }                                     file_userdata.set("__position", position)?;                                     if line.is_empty() && position >= content_vec.len() {                                        return Ok(None); // EOF                                    }                                     let result = String::from_utf8_lossy(&line).to_string();                                    Ok(Some(result))                                }                                _ => Err(mlua::Error::runtime(format!(                                    "Unsupported format: {}",                                    format_str                                ))),                            }                        }                        Some(mlua::Value::Number(n)) => {                            // Read n bytes                            let n = n as usize;                            let end = std::cmp::min(position + n, content_vec.len());                            let bytes = &content_vec[position..end];                            let result = String::from_utf8_lossy(bytes).to_string();                            position = end;                            file_userdata.set("__position", position)?;                            Ok(Some(result))                        }                        Some(_) => Err(mlua::Error::runtime("Invalid format")),                        None => {                            // Default is to read a line                            let mut line = Vec::new();                            let mut found_newline = false;                             while position < content_vec.len() {                                let byte = content_vec[position];                                position += 1;                                 if byte == b'\n' {                                    found_newline = true;                                    break;                                }                                 // Handle \r\n                                if byte == b'\r' {                                    if position < content_vec.len()

Lines 1–500 of 945.