2301 lines · 77.3 KB · Rust
RawDownload
use anyhow::Result;use collections::{HashMap, HashSet};use command_palette_hooks::CommandInterceptResult;use editor::{    Bias, Editor, SelectionEffects, ToPoint,    actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},    display_map::ToDisplayPoint,};use gpui::{Action, App, AppContext as _, Context, Global, Window, actions};use itertools::Itertools;use language::Point;use multi_buffer::MultiBufferRow;use project::ProjectPath;use regex::Regex;use schemars::JsonSchema;use search::{BufferSearchBar, SearchOptions};use serde::Deserialize;use std::{    io::Write,    iter::Peekable,    ops::{Deref, Range},    path::Path,    process::Stdio,    str::Chars,    sync::{Arc, OnceLock},    time::Instant,};use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};use ui::ActiveTheme;use util::ResultExt;use workspace::{Item, SaveIntent, notifications::NotifyResultExt};use workspace::{SplitDirection, notifications::DetachAndPromptErr};use zed_actions::{OpenDocs, RevealTarget}; use crate::{    ToggleMarksView, ToggleRegistersView, Vim,    motion::{EndOfDocument, Motion, MotionKind, StartOfDocument},    normal::{        JoinLines,        search::{FindCommand, ReplaceCommand, Replacement},    },    object::Object,    state::{Mark, Mode},    visual::VisualDeleteLine,}; /// Goes to the specified line number in the editor.#[derive(Clone, Debug, PartialEq, Action)]#[action(namespace = vim, no_json, no_register)]pub struct GoToLine {    range: CommandRange,} /// Yanks (copies) text based on the specified range.#[derive(Clone, Debug, PartialEq, Action)]#[action(namespace = vim, no_json, no_register)]pub struct YankCommand {    range: CommandRange,} /// Executes a command with the specified range.#[derive(Clone, Debug, PartialEq, Action)]#[action(namespace = vim, no_json, no_register)]pub struct WithRange {    restore_selection: bool,    range: CommandRange,    action: WrappedAction,} /// Executes a command with the specified count.#[derive(Clone, Debug, PartialEq, Action)]#[action(namespace = vim, no_json, no_register)]pub struct WithCount {    count: u32,    action: WrappedAction,} #[derive(Clone, Deserialize, JsonSchema, PartialEq)]pub enum VimOption {    Wrap(bool),    Number(bool),    RelativeNumber(bool),} impl VimOption {    fn possible_commands(query: &str) -> Vec<CommandInterceptResult> {        let mut prefix_of_options = Vec::new();        let mut options = query.split(" ").collect::<Vec<_>>();        let prefix = options.pop().unwrap_or_default();        for option in options {            if let Some(opt) = Self::from(option) {                prefix_of_options.push(opt)            } else {                return vec![];            }        }         Self::possibilities(&prefix)            .map(|possible| {                let mut options = prefix_of_options.clone();                options.push(possible);                 CommandInterceptResult {                    string: format!(                        ":set {}",                        options.iter().map(|opt| opt.to_string()).join(" ")                    ),                    action: VimSet { options }.boxed_clone(),                    positions: vec![],                }            })            .collect()    }     fn possibilities(query: &str) -> impl Iterator<Item = Self> + '_ {        [            (None, VimOption::Wrap(true)),            (None, VimOption::Wrap(false)),            (None, VimOption::Number(true)),            (None, VimOption::Number(false)),            (None, VimOption::RelativeNumber(true)),            (None, VimOption::RelativeNumber(false)),            (Some("rnu"), VimOption::RelativeNumber(true)),            (Some("nornu"), VimOption::RelativeNumber(false)),        ]        .into_iter()        .filter(move |(prefix, option)| prefix.unwrap_or(option.to_string()).starts_with(query))        .map(|(_, option)| option)    }     fn from(option: &str) -> Option<Self> {        match option {            "wrap" => Some(Self::Wrap(true)),            "nowrap" => Some(Self::Wrap(false)),             "number" => Some(Self::Number(true)),            "nu" => Some(Self::Number(true)),            "nonumber" => Some(Self::Number(false)),            "nonu" => Some(Self::Number(false)),             "relativenumber" => Some(Self::RelativeNumber(true)),            "rnu" => Some(Self::RelativeNumber(true)),            "norelativenumber" => Some(Self::RelativeNumber(false)),            "nornu" => Some(Self::RelativeNumber(false)),             _ => None,        }    }     fn to_string(&self) -> &'static str {        match self {            VimOption::Wrap(true) => "wrap",            VimOption::Wrap(false) => "nowrap",            VimOption::Number(true) => "number",            VimOption::Number(false) => "nonumber",            VimOption::RelativeNumber(true) => "relativenumber",            VimOption::RelativeNumber(false) => "norelativenumber",        }    }} /// Sets vim options and configuration values.#[derive(Clone, PartialEq, Action)]#[action(namespace = vim, no_json, no_register)]pub struct VimSet {    options: Vec<VimOption>,} /// Saves the current file with optional save intent.#[derive(Clone, PartialEq, Action)]#[action(namespace = vim, no_json, no_register)]struct VimSave {    pub save_intent: Option<SaveIntent>,    pub filename: String,} /// Deletes the specified marks from the editor.#[derive(Clone, PartialEq, Action)]#[action(namespace = vim, no_json, no_register)]struct VimSplit {    pub vertical: bool,    pub filename: String,} #[derive(Clone, PartialEq, Action)]#[action(namespace = vim, no_json, no_register)]enum DeleteMarks {    Marks(String),    AllLocal,} actions!(    vim,    [        /// Executes a command in visual mode.        VisualCommand,        /// Executes a command with a count prefix.        CountCommand,        /// Executes a shell command.        ShellCommand,        /// Indicates that an argument is required for the command.        ArgumentRequired    ]);/// Opens the specified file for editing.#[derive(Clone, PartialEq, Action)]#[action(namespace = vim, no_json, no_register)]struct VimEdit {    pub filename: String,} #[derive(Debug)]struct WrappedAction(Box<dyn Action>); impl PartialEq for WrappedAction {    fn eq(&self, other: &Self) -> bool {        self.0.partial_eq(&*other.0)    }} impl Clone for WrappedAction {    fn clone(&self) -> Self {        Self(self.0.boxed_clone())    }} impl Deref for WrappedAction {    type Target = dyn Action;    fn deref(&self) -> &dyn Action {        &*self.0    }} pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {    // Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {    Vim::action(editor, cx, |vim, action: &VimSet, window, cx| {        for option in action.options.iter() {            vim.update_editor(window, cx, |_, editor, _, cx| match option {                VimOption::Wrap(true) => {                    editor                        .set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);                }                VimOption::Wrap(false) => {                    editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);                }                VimOption::Number(enabled) => {                    editor.set_show_line_numbers(*enabled, cx);                }                VimOption::RelativeNumber(enabled) => {                    editor.set_relative_line_number(Some(*enabled), cx);                }            });        }    });    Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| {        let Some(workspace) = vim.workspace(window) else {            return;        };        workspace.update(cx, |workspace, cx| {            command_palette::CommandPalette::toggle(workspace, "'<,'>", window, cx);        })    });     Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {        let Some(workspace) = vim.workspace(window) else {            return;        };        workspace.update(cx, |workspace, cx| {            command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx);        })    });     Vim::action(editor, cx, |_, _: &ArgumentRequired, window, cx| {        let _ = window.prompt(            gpui::PromptLevel::Critical,            "Argument required",            None,            &["Cancel"],            cx,        );    });     Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| {        let Some(workspace) = vim.workspace(window) else {            return;        };        workspace.update(cx, |workspace, cx| {            command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx);        })    });     Vim::action(editor, cx, |vim, action: &VimSave, window, cx| {        vim.update_editor(window, cx, |_, editor, window, cx| {            let Some(project) = editor.project.clone() else {                return;            };            let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {                return;            };            let project_path = ProjectPath {                worktree_id: worktree.read(cx).id(),                path: Arc::from(Path::new(&action.filename)),            };             if project.read(cx).entry_for_path(&project_path, cx).is_some() && action.save_intent != Some(SaveIntent::Overwrite) {                let answer = window.prompt(                    gpui::PromptLevel::Critical,                    &format!("{} already exists. Do you want to replace it?", project_path.path.to_string_lossy()),                    Some(                        "A file or folder with the same name already exists. Replacing it will overwrite its current contents.",                    ),                    &["Replace", "Cancel"],                cx);                cx.spawn_in(window, async move |editor, cx| {                    if answer.await.ok() != Some(0) {                        return;                    }                     let _ = editor.update_in(cx, |editor, window, cx|{                        editor                            .save_as(project, project_path, window, cx)                            .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);                    });                }).detach();            } else {                editor                    .save_as(project, project_path, window, cx)                    .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None);            }        });    });     Vim::action(editor, cx, |vim, action: &VimSplit, window, cx| {        let Some(workspace) = vim.workspace(window) else {            return;        };         workspace.update(cx, |workspace, cx| {            let project = workspace.project().clone();            let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {                return;            };            let project_path = ProjectPath {                worktree_id: worktree.read(cx).id(),                path: Arc::from(Path::new(&action.filename)),            };             let direction = if action.vertical {                SplitDirection::vertical(cx)            } else {                SplitDirection::horizontal(cx)            };             workspace                .split_path_preview(project_path, false, Some(direction), window, cx)                .detach_and_log_err(cx);        })    });     Vim::action(editor, cx, |vim, action: &DeleteMarks, window, cx| {        fn err(s: String, window: &mut Window, cx: &mut Context<Editor>) {            let _ = window.prompt(                gpui::PromptLevel::Critical,                &format!("Invalid argument: {}", s),                None,                &["Cancel"],                cx,            );        }        vim.update_editor(window, cx, |vim, editor, window, cx| match action {            DeleteMarks::Marks(s) => {                if s.starts_with('-') || s.ends_with('-') || s.contains(['\'', '`']) {                    err(s.clone(), window, cx);                    return;                }                 let to_delete = if s.len() < 3 {                    Some(s.clone())                } else {                    s.chars()                        .tuple_windows::<(_, _, _)>()                        .map(|(a, b, c)| {                            if b == '-' {                                if match a {                                    'a'..='z' => a <= c && c <= 'z',                                    'A'..='Z' => a <= c && c <= 'Z',                                    '0'..='9' => a <= c && c <= '9',                                    _ => false,                                } {                                    Some((a..=c).collect_vec())                                } else {                                    None                                }                            } else if a == '-' {                                if c == '-' { None } else { Some(vec![c]) }                            } else if c == '-' {                                if a == '-' { None } else { Some(vec![a]) }                            } else {                                Some(vec![a, b, c])                            }                        })                        .fold_options(HashSet::<char>::default(), |mut set, chars| {                            set.extend(chars.iter().copied());                            set                        })                        .map(|set| set.iter().collect::<String>())                };                 let Some(to_delete) = to_delete else {                    err(s.clone(), window, cx);                    return;                };                 for c in to_delete.chars().filter(|c| !c.is_whitespace()) {                    vim.delete_mark(c.to_string(), editor, window, cx);                }            }            DeleteMarks::AllLocal => {                for s in 'a'..='z' {                    vim.delete_mark(s.to_string(), editor, window, cx);                }            }        });    });     Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| {        vim.update_editor(window, cx, |vim, editor, window, cx| {            let Some(workspace) = vim.workspace(window) else {                return;            };            let Some(project) = editor.project.clone() else {                return;            };            let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {                return;            };            let project_path = ProjectPath {                worktree_id: worktree.read(cx).id(),                path: Arc::from(Path::new(&action.filename)),            };             let _ = workspace.update(cx, |workspace, cx| {                workspace                    .open_path(project_path, None, true, window, cx)                    .detach_and_log_err(cx);            });        });    });     Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| {        let Some(workspace) = vim.workspace(window) else {            return;        };        let count = Vim::take_count(cx).unwrap_or(1);        Vim::take_forced_motion(cx);        let n = if count > 1 {            format!(".,.+{}", count.saturating_sub(1))        } else {            ".".to_string()        };        workspace.update(cx, |workspace, cx| {            command_palette::CommandPalette::toggle(workspace, &n, window, cx);        })    });     Vim::action(editor, cx, |vim, action: &GoToLine, window, cx| {        vim.switch_mode(Mode::Normal, false, window, cx);        let result = vim.update_editor(window, cx, |vim, editor, window, cx| {            let snapshot = editor.snapshot(window, cx);            let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?;            let current = editor.selections.newest::<Point>(cx);            let target = snapshot                .buffer_snapshot                .clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left);            editor.change_selections(Default::default(), window, cx, |s| {                s.select_ranges([target..target]);            });             anyhow::Ok(())        });        if let Some(e @ Err(_)) = result {            let Some(workspace) = vim.workspace(window) else {                return;            };            workspace.update(cx, |workspace, cx| {                e.notify_err(workspace, cx);            });            return;        }    });     Vim::action(editor, cx, |vim, action: &YankCommand, window, cx| {        vim.update_editor(window, cx, |vim, editor, window, cx| {            let snapshot = editor.snapshot(window, cx);            if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) {                let end = if range.end < snapshot.buffer_snapshot.max_row() {                    Point::new(range.end.0 + 1, 0)                } else {                    snapshot.buffer_snapshot.max_point()                };

Lines 1–500 of 2301.