use anyhow::{anyhow, Result};use collections::HashMap;use command_palette_hooks::CommandInterceptResult;use editor::{ actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive}, display_map::ToDisplayPoint, scroll::Autoscroll, Bias, Editor, ToPoint,};use gpui::{actions, impl_internal_actions, Action, App, AppContext as _, Context, Global, Window};use itertools::Itertools;use language::Point;use multi_buffer::MultiBufferRow;use regex::Regex;use schemars::JsonSchema;use search::{BufferSearchBar, SearchOptions};use serde::Deserialize;use std::{ io::Write, iter::Peekable, ops::{Deref, Range}, process::Stdio, str::Chars, sync::OnceLock, time::Instant,};use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};use ui::ActiveTheme;use util::ResultExt;use workspace::{notifications::NotifyResultExt, SaveIntent};use zed_actions::RevealTarget; use crate::{ motion::{EndOfDocument, Motion, StartOfDocument}, normal::{ search::{FindCommand, ReplaceCommand, Replacement}, JoinLines, }, object::Object, state::Mode, visual::VisualDeleteLine, ToggleRegistersView, Vim,}; #[derive(Clone, Debug, PartialEq)]pub struct GoToLine { range: CommandRange,} #[derive(Clone, Debug, PartialEq)]pub struct YankCommand { range: CommandRange,} #[derive(Clone, Debug, PartialEq)]pub struct WithRange { restore_selection: bool, range: CommandRange, action: WrappedAction,} #[derive(Clone, Debug, PartialEq)]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", } }} #[derive(Clone, Deserialize, JsonSchema, PartialEq)]pub struct VimSet { options: Vec<VimOption>,} #[derive(Debug)]struct WrappedAction(Box<dyn Action>); actions!(vim, [VisualCommand, CountCommand, ShellCommand]);impl_internal_actions!( vim, [ GoToLine, YankCommand, WithRange, WithCount, OnMatchingLines, ShellExec, VimSet, ]); 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, |vim, _: &CountCommand, window, cx| { let Some(workspace) = vim.workspace(window) else { return; }; let count = Vim::take_count(cx).unwrap_or(1); 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(Some(Autoscroll::fit()), 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() }; vim.copy_ranges( editor, true, true, vec![Point::new(range.start.0, 0)..end], cx, ) } }); }); Vim::action(editor, cx, |_, action: &WithCount, window, cx| { for _ in 0..action.count { window.dispatch_action(action.action.boxed_clone(), cx) } }); Vim::action(editor, cx, |vim, action: &WithRange, window, cx| { let result = vim.update_editor(window, cx, |vim, editor, window, cx| { action.range.buffer_range(vim, editor, window, cx) }); let range = match result { None => return, Some(e @ Err(_)) => { let Some(workspace) = vim.workspace(window) else { return; }; workspace.update(cx, |workspace, cx| { e.notify_err(workspace, cx); }); return; } Some(Ok(result)) => result, }; let previous_selections = vim .update_editor(window, cx, |_, editor, window, cx| { let selections = action.restore_selection.then(|| { editor .selections .disjoint_anchor_ranges() .collect::<Vec<_>>() }); editor.change_selections(None, window, cx, |s| { let end = Point::new(range.end.0, s.buffer().line_len(range.end)); s.select_ranges([end..Point::new(range.start.0, 0)]); }); selections }) .flatten(); window.dispatch_action(action.action.boxed_clone(), cx); cx.defer_in(window, move |vim, window, cx| { vim.update_editor(window, cx, |_, editor, window, cx| { editor.change_selections(None, window, cx, |s| { if let Some(previous_selections) = previous_selections { s.select_ranges(previous_selections); } else { s.select_ranges([ Point::new(range.start.0, 0)..Point::new(range.start.0, 0) ]); } }) }); }); }); Vim::action(editor, cx, |vim, action: &OnMatchingLines, window, cx| { action.run(vim, window, cx) }); Vim::action(editor, cx, |vim, action: &ShellExec, window, cx| { action.run(vim, window, cx) })} #[derive(Default)]struct VimCommand { prefix: &'static str, suffix: &'static str, action: Option<Box<dyn Action>>, action_name: Option<&'static str>, bang_action: Option<Box<dyn Action>>, range: Option< Box< dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static, >, >, has_count: bool,} impl VimCommand { fn new(pattern: (&'static str, &'static str), action: impl Action) -> Self { Self { prefix: pattern.0, suffix: pattern.1, action: Some(action.boxed_clone()), ..Default::default() } } // from_str is used for actions in other crates. fn str(pattern: (&'static str, &'static str), action_name: &'static str) -> Self { Self { prefix: pattern.0, suffix: pattern.1, action_name: Some(action_name), ..Default::default() } } fn bang(mut self, bang_action: impl Action) -> Self { self.bang_action = Some(bang_action.boxed_clone()); self } fn range( mut self, f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static, ) -> Self { self.range = Some(Box::new(f)); self } fn count(mut self) -> Self { self.has_count = true; self } fn parse( &self, mut query: &str, range: &Option<CommandRange>, cx: &App, ) -> Option<Box<dyn Action>> { let has_bang = query.ends_with('!'); if has_bang { query = &query[..query.len() - 1]; } let suffix = query.strip_prefix(self.prefix)?; if !self.suffix.starts_with(suffix) { return None; } let action = if has_bang && self.bang_action.is_some() { self.bang_action.as_ref().unwrap().boxed_clone() } else if let Some(action) = self.action.as_ref() { action.boxed_clone() } else if let Some(action_name) = self.action_name { cx.build_action(action_name, None).log_err()? } else { return None; }; if let Some(range) = range { self.range.as_ref().and_then(|f| f(action, range)) } else { Some(action) } } // TODO: ranges with search queries fn parse_range(query: &str) -> (Option<CommandRange>, String) { let mut chars = query.chars().peekable(); match chars.peek() { Some('%') => { chars.next(); return ( Some(CommandRange { start: Position::Line { row: 1, offset: 0 }, end: Some(Position::LastLine { offset: 0 }), }), chars.collect(), ); } Some('*') => { chars.next(); return ( Some(CommandRange { start: Position::Mark { name: '<', offset: 0, }, end: Some(Position::Mark { name: '>', offset: 0, }), }), chars.collect(), ); } _ => {} } let start = Self::parse_position(&mut chars); match chars.peek() { Some(',' | ';') => { chars.next(); ( Some(CommandRange { start: start.unwrap_or(Position::CurrentLine { offset: 0 }), end: Self::parse_position(&mut chars), }), chars.collect(), ) } _ => ( start.map(|start| CommandRange { start, end: None }), chars.collect(), ), } } Lines 1–500 of 1899. Load more
use anyhow::{anyhow, Result}; use collections::HashMap; use command_palette_hooks::CommandInterceptResult; use editor::{ actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive}, display_map::ToDisplayPoint, scroll::Autoscroll, Bias, Editor, ToPoint, }; use gpui::{actions, impl_internal_actions, Action, App, AppContext as _, Context, Global, Window}; use itertools::Itertools; use language::Point; use multi_buffer::MultiBufferRow; use regex::Regex; use schemars::JsonSchema; use search::{BufferSearchBar, SearchOptions}; use serde::Deserialize; use std::{ io::Write, iter::Peekable, ops::{Deref, Range}, process::Stdio, str::Chars, sync::OnceLock, time::Instant, }; use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId}; use ui::ActiveTheme; use util::ResultExt; use workspace::{notifications::NotifyResultExt, SaveIntent}; use zed_actions::RevealTarget; use crate::{ motion::{EndOfDocument, Motion, StartOfDocument}, normal::{ search::{FindCommand, ReplaceCommand, Replacement}, JoinLines, }, object::Object, state::Mode, visual::VisualDeleteLine, ToggleRegistersView, Vim, }; #[derive(Clone, Debug, PartialEq)] pub struct GoToLine { range: CommandRange, } #[derive(Clone, Debug, PartialEq)] pub struct YankCommand { range: CommandRange, } #[derive(Clone, Debug, PartialEq)] pub struct WithRange { restore_selection: bool, range: CommandRange, action: WrappedAction, } #[derive(Clone, Debug, PartialEq)] 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", } } } #[derive(Clone, Deserialize, JsonSchema, PartialEq)] pub struct VimSet { options: Vec<VimOption>, } #[derive(Debug)] struct WrappedAction(Box<dyn Action>); actions!(vim, [VisualCommand, CountCommand, ShellCommand]); impl_internal_actions!( vim, [ GoToLine, YankCommand, WithRange, WithCount, OnMatchingLines, ShellExec, VimSet, ] ); 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, |vim, _: &CountCommand, window, cx| { let Some(workspace) = vim.workspace(window) else { return; }; let count = Vim::take_count(cx).unwrap_or(1); 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(Some(Autoscroll::fit()), 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() }; vim.copy_ranges( editor, true, true, vec![Point::new(range.start.0, 0)..end], cx, ) } }); }); Vim::action(editor, cx, |_, action: &WithCount, window, cx| { for _ in 0..action.count { window.dispatch_action(action.action.boxed_clone(), cx) } }); Vim::action(editor, cx, |vim, action: &WithRange, window, cx| { let result = vim.update_editor(window, cx, |vim, editor, window, cx| { action.range.buffer_range(vim, editor, window, cx) }); let range = match result { None => return, Some(e @ Err(_)) => { let Some(workspace) = vim.workspace(window) else { return; }; workspace.update(cx, |workspace, cx| { e.notify_err(workspace, cx); }); return; } Some(Ok(result)) => result, }; let previous_selections = vim .update_editor(window, cx, |_, editor, window, cx| { let selections = action.restore_selection.then(|| { editor .selections .disjoint_anchor_ranges() .collect::<Vec<_>>() }); editor.change_selections(None, window, cx, |s| { let end = Point::new(range.end.0, s.buffer().line_len(range.end)); s.select_ranges([end..Point::new(range.start.0, 0)]); }); selections }) .flatten(); window.dispatch_action(action.action.boxed_clone(), cx); cx.defer_in(window, move |vim, window, cx| { vim.update_editor(window, cx, |_, editor, window, cx| { editor.change_selections(None, window, cx, |s| { if let Some(previous_selections) = previous_selections { s.select_ranges(previous_selections); } else { s.select_ranges([ Point::new(range.start.0, 0)..Point::new(range.start.0, 0) ]); } }) }); }); }); Vim::action(editor, cx, |vim, action: &OnMatchingLines, window, cx| { action.run(vim, window, cx) }); Vim::action(editor, cx, |vim, action: &ShellExec, window, cx| { action.run(vim, window, cx) }) } #[derive(Default)] struct VimCommand { prefix: &'static str, suffix: &'static str, action: Option<Box<dyn Action>>, action_name: Option<&'static str>, bang_action: Option<Box<dyn Action>>, range: Option< Box< dyn Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static, >, >, has_count: bool, } impl VimCommand { fn new(pattern: (&'static str, &'static str), action: impl Action) -> Self { Self { prefix: pattern.0, suffix: pattern.1, action: Some(action.boxed_clone()), ..Default::default() } } // from_str is used for actions in other crates. fn str(pattern: (&'static str, &'static str), action_name: &'static str) -> Self { Self { prefix: pattern.0, suffix: pattern.1, action_name: Some(action_name), ..Default::default() } } fn bang(mut self, bang_action: impl Action) -> Self { self.bang_action = Some(bang_action.boxed_clone()); self } fn range( mut self, f: impl Fn(Box<dyn Action>, &CommandRange) -> Option<Box<dyn Action>> + Send + Sync + 'static, ) -> Self { self.range = Some(Box::new(f)); self } fn count(mut self) -> Self { self.has_count = true; self } fn parse( &self, mut query: &str, range: &Option<CommandRange>, cx: &App, ) -> Option<Box<dyn Action>> { let has_bang = query.ends_with('!'); if has_bang { query = &query[..query.len() - 1]; } let suffix = query.strip_prefix(self.prefix)?; if !self.suffix.starts_with(suffix) { return None; } let action = if has_bang && self.bang_action.is_some() { self.bang_action.as_ref().unwrap().boxed_clone() } else if let Some(action) = self.action.as_ref() { action.boxed_clone() } else if let Some(action_name) = self.action_name { cx.build_action(action_name, None).log_err()? } else { return None; }; if let Some(range) = range { self.range.as_ref().and_then(|f| f(action, range)) } else { Some(action) } } // TODO: ranges with search queries fn parse_range(query: &str) -> (Option<CommandRange>, String) { let mut chars = query.chars().peekable(); match chars.peek() { Some('%') => { chars.next(); return ( Some(CommandRange { start: Position::Line { row: 1, offset: 0 }, end: Some(Position::LastLine { offset: 0 }), }), chars.collect(), ); } Some('*') => { chars.next(); return ( Some(CommandRange { start: Position::Mark { name: '<', offset: 0, }, end: Some(Position::Mark { name: '>', offset: 0, }), }), chars.collect(), ); } _ => {} } let start = Self::parse_position(&mut chars); match chars.peek() { Some(',' | ';') => { chars.next(); ( Some(CommandRange { start: start.unwrap_or(Position::CurrentLine { offset: 0 }), end: Self::parse_position(&mut chars), }), chars.collect(), ) } _ => ( start.map(|start| CommandRange { start, end: None }), chars.collect(), ), } }