use anyhow::{Result, anyhow};use collections::{HashMap, HashSet};use command_palette_hooks::{CommandInterceptItem, CommandInterceptResult};use editor::{ Bias, Editor, EditorSettings, SelectionEffects, ToPoint, actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive}, display_map::ToDisplayPoint,};use futures::AsyncWriteExt as _;use gpui::{ Action, App, AppContext as _, Context, Global, Keystroke, Task, WeakEntity, 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 settings::{Settings, SettingsStore};use std::{ iter::Peekable, ops::{Deref, Range}, path::{Path, PathBuf}, process::Stdio, str::Chars, sync::OnceLock, time::Instant,};use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};use ui::ActiveTheme;use util::{ ResultExt, paths::PathStyle, rel_path::{RelPath, RelPathBuf},};use workspace::{Item, SaveIntent, Workspace, notifications::NotifyResultExt};use workspace::{SplitDirection, notifications::DetachAndPromptErr};use zed_actions::{OpenDocs, RevealTarget}; use crate::{ ToggleMarksView, ToggleRegistersView, Vim, VimSettings, 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), IgnoreCase(bool), GDefault(bool),} impl VimOption { fn possible_commands(query: &str) -> Vec<CommandInterceptItem> { 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); CommandInterceptItem { 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)), (None, VimOption::IgnoreCase(true)), (None, VimOption::IgnoreCase(false)), (Some("ic"), VimOption::IgnoreCase(true)), (Some("noic"), VimOption::IgnoreCase(false)), (None, VimOption::GDefault(true)), (Some("gd"), VimOption::GDefault(true)), (None, VimOption::GDefault(false)), (Some("nogd"), VimOption::GDefault(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)), "ignorecase" => Some(Self::IgnoreCase(true)), "ic" => Some(Self::IgnoreCase(true)), "noignorecase" => Some(Self::IgnoreCase(false)), "noic" => Some(Self::IgnoreCase(false)), "gdefault" => Some(Self::GDefault(true)), "gd" => Some(Self::GDefault(true)), "nogdefault" => Some(Self::GDefault(false)), "nogd" => Some(Self::GDefault(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", VimOption::IgnoreCase(true) => "ignorecase", VimOption::IgnoreCase(false) => "noignorecase", VimOption::GDefault(true) => "gdefault", VimOption::GDefault(false) => "nogdefault", } }} /// 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 range: Option<CommandRange>, 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,} /// Pastes the specified file's contents.#[derive(Clone, PartialEq, Action)]#[action(namespace = vim, no_json, no_register)]struct VimRead { pub range: Option<CommandRange>, pub filename: String,} #[derive(Clone, PartialEq, Action)]#[action(namespace = vim, no_json, no_register)]struct VimNorm { pub range: Option<CommandRange>, pub command: String, /// Places cursors at beginning of each given row. /// Overrides given range and current cursor. pub override_rows: Option<Vec<u32>>,} #[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: &VimSet, _, cx| { for option in action.options.iter() { vim.update_editor(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); } VimOption::IgnoreCase(enabled) => { let mut settings = EditorSettings::get_global(cx).clone(); settings.search.case_sensitive = !*enabled; SettingsStore::update(cx, |store, _| { store.override_global(settings); }); } VimOption::GDefault(enabled) => { let mut settings = VimSettings::get_global(cx).clone(); settings.gdefault = *enabled; SettingsStore::update(cx, |store, _| { store.override_global(settings); }) } }); } }); Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| { let Some(workspace) = vim.workspace(window, cx) 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, cx) 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, cx) else { return; }; workspace.update(cx, |workspace, cx| { command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx); }) }); Vim::action(editor, cx, |vim, action: &VimSave, window, cx| { if let Some(range) = &action.range { vim.update_editor(cx, |vim, editor, cx| { let Some(range) = range.buffer_range(vim, editor, window, cx).ok() else { return; }; let Some((line_ending, encoding, has_bom, text, whole_buffer)) = editor.buffer().update(cx, |multi, cx| { Some(multi.as_singleton()?.update(cx, |buffer, _| { ( buffer.line_ending(), buffer.encoding(), buffer.has_bom(), buffer.as_rope().slice_rows(range.start.0..range.end.0 + 1), range.start.0 == 0 && range.end.0 + 1 >= buffer.row_count(), ) })) }) else { return; }; let filename = action.filename.clone(); let filename = if filename.is_empty() { let Some(file) = editor .buffer() .read(cx) .as_singleton() .and_then(|buffer| buffer.read(cx).file()) else { let _ = window.prompt( gpui::PromptLevel::Warning, "No file name", Some("Partial buffer write requires file name."), &["Cancel"], cx, ); return; }; file.path().display(file.path_style(cx)).to_string() } else { filename }; if action.filename.is_empty() { if whole_buffer { if let Some(workspace) = vim.workspace(window, cx) { workspace.update(cx, |workspace, cx| { workspace .save_active_item( action.save_intent.unwrap_or(SaveIntent::Save), window, cx, ) .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); }); } return; } if Some(SaveIntent::Overwrite) != action.save_intent { let _ = window.prompt( gpui::PromptLevel::Warning, "Use ! to write partial buffer", Some("Overwriting the current file with selected buffer content requires '!'."), &["Cancel"], cx, ); return; } editor.buffer().update(cx, |multi, cx| { if let Some(buffer) = multi.as_singleton() { buffer.update(cx, |buffer, _| buffer.set_conflict()); } }); }; editor.project().unwrap().update(cx, |project, cx| { let worktree = project.visible_worktrees(cx).next().unwrap(); worktree.update(cx, |worktree, cx| { let path_style = worktree.path_style(); let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else { return; }; let rx = (worktree.entry_for_path(&path).is_some() && Some(SaveIntent::Overwrite) != action.save_intent).then(|| { window.prompt( gpui::PromptLevel::Warning, &format!("{path:?} already exists. Do you want to replace it?"), Some( "A file or folder with the same name already exists. Replacing it will overwrite its current contents.", ), &["Replace", "Cancel"], cx ) }); let filename = filename.clone(); cx.spawn_in(window, async move |this, cx| { if let Some(rx) = rx && Ok(0) != rx.await { return; } let _ = this.update_in(cx, |worktree, window, cx| { let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else { return; }; worktree .write_file(path.into_arc(), text.clone(), line_ending, encoding, has_bom, cx) .detach_and_prompt_err("Failed to write lines", window, cx, |_, _, _| None); }); }) .detach(); }); }); }); return; } if action.filename.is_empty() { if let Some(workspace) = vim.workspace(window, cx) { workspace.update(cx, |workspace, cx| { workspace .save_active_item( action.save_intent.unwrap_or(SaveIntent::Save), window, cx, ) .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); }); } return; } vim.update_editor(cx, |_, editor, cx| { let Some(project) = editor.project().cloned() else { return; }; let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else { return; }; let path_style = worktree.read(cx).path_style(); let Ok(project_path) = RelPath::new(Path::new(&action.filename), path_style).map(|path| ProjectPath { worktree_id: worktree.read(cx).id(), path: path.into_arc(), })Lines 1–500 of 3539. Load more
use anyhow::{Result, anyhow}; use collections::{HashMap, HashSet}; use command_palette_hooks::{CommandInterceptItem, CommandInterceptResult}; use editor::{ Bias, Editor, EditorSettings, SelectionEffects, ToPoint, actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive}, display_map::ToDisplayPoint, }; use futures::AsyncWriteExt as _; use gpui::{ Action, App, AppContext as _, Context, Global, Keystroke, Task, WeakEntity, 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 settings::{Settings, SettingsStore}; use std::{ iter::Peekable, ops::{Deref, Range}, path::{Path, PathBuf}, process::Stdio, str::Chars, sync::OnceLock, time::Instant, }; use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId}; use ui::ActiveTheme; use util::{ ResultExt, paths::PathStyle, rel_path::{RelPath, RelPathBuf}, }; use workspace::{Item, SaveIntent, Workspace, notifications::NotifyResultExt}; use workspace::{SplitDirection, notifications::DetachAndPromptErr}; use zed_actions::{OpenDocs, RevealTarget}; use crate::{ ToggleMarksView, ToggleRegistersView, Vim, VimSettings, 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), IgnoreCase(bool), GDefault(bool), } impl VimOption { fn possible_commands(query: &str) -> Vec<CommandInterceptItem> { 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); CommandInterceptItem { 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)), (None, VimOption::IgnoreCase(true)), (None, VimOption::IgnoreCase(false)), (Some("ic"), VimOption::IgnoreCase(true)), (Some("noic"), VimOption::IgnoreCase(false)), (None, VimOption::GDefault(true)), (Some("gd"), VimOption::GDefault(true)), (None, VimOption::GDefault(false)), (Some("nogd"), VimOption::GDefault(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)), "ignorecase" => Some(Self::IgnoreCase(true)), "ic" => Some(Self::IgnoreCase(true)), "noignorecase" => Some(Self::IgnoreCase(false)), "noic" => Some(Self::IgnoreCase(false)), "gdefault" => Some(Self::GDefault(true)), "gd" => Some(Self::GDefault(true)), "nogdefault" => Some(Self::GDefault(false)), "nogd" => Some(Self::GDefault(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", VimOption::IgnoreCase(true) => "ignorecase", VimOption::IgnoreCase(false) => "noignorecase", VimOption::GDefault(true) => "gdefault", VimOption::GDefault(false) => "nogdefault", } } } /// 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 range: Option<CommandRange>, 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, } /// Pastes the specified file's contents. #[derive(Clone, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] struct VimRead { pub range: Option<CommandRange>, pub filename: String, } #[derive(Clone, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] struct VimNorm { pub range: Option<CommandRange>, pub command: String, /// Places cursors at beginning of each given row. /// Overrides given range and current cursor. pub override_rows: Option<Vec<u32>>, } #[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: &VimSet, _, cx| { for option in action.options.iter() { vim.update_editor(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); } VimOption::IgnoreCase(enabled) => { let mut settings = EditorSettings::get_global(cx).clone(); settings.search.case_sensitive = !*enabled; SettingsStore::update(cx, |store, _| { store.override_global(settings); }); } VimOption::GDefault(enabled) => { let mut settings = VimSettings::get_global(cx).clone(); settings.gdefault = *enabled; SettingsStore::update(cx, |store, _| { store.override_global(settings); }) } }); } }); Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| { let Some(workspace) = vim.workspace(window, cx) 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, cx) 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, cx) else { return; }; workspace.update(cx, |workspace, cx| { command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx); }) }); Vim::action(editor, cx, |vim, action: &VimSave, window, cx| { if let Some(range) = &action.range { vim.update_editor(cx, |vim, editor, cx| { let Some(range) = range.buffer_range(vim, editor, window, cx).ok() else { return; }; let Some((line_ending, encoding, has_bom, text, whole_buffer)) = editor.buffer().update(cx, |multi, cx| { Some(multi.as_singleton()?.update(cx, |buffer, _| { ( buffer.line_ending(), buffer.encoding(), buffer.has_bom(), buffer.as_rope().slice_rows(range.start.0..range.end.0 + 1), range.start.0 == 0 && range.end.0 + 1 >= buffer.row_count(), ) })) }) else { return; }; let filename = action.filename.clone(); let filename = if filename.is_empty() { let Some(file) = editor .buffer() .read(cx) .as_singleton() .and_then(|buffer| buffer.read(cx).file()) else { let _ = window.prompt( gpui::PromptLevel::Warning, "No file name", Some("Partial buffer write requires file name."), &["Cancel"], cx, ); return; }; file.path().display(file.path_style(cx)).to_string() } else { filename }; if action.filename.is_empty() { if whole_buffer { if let Some(workspace) = vim.workspace(window, cx) { workspace.update(cx, |workspace, cx| { workspace .save_active_item( action.save_intent.unwrap_or(SaveIntent::Save), window, cx, ) .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); }); } return; } if Some(SaveIntent::Overwrite) != action.save_intent { let _ = window.prompt( gpui::PromptLevel::Warning, "Use ! to write partial buffer", Some("Overwriting the current file with selected buffer content requires '!'."), &["Cancel"], cx, ); return; } editor.buffer().update(cx, |multi, cx| { if let Some(buffer) = multi.as_singleton() { buffer.update(cx, |buffer, _| buffer.set_conflict()); } }); }; editor.project().unwrap().update(cx, |project, cx| { let worktree = project.visible_worktrees(cx).next().unwrap(); worktree.update(cx, |worktree, cx| { let path_style = worktree.path_style(); let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else { return; }; let rx = (worktree.entry_for_path(&path).is_some() && Some(SaveIntent::Overwrite) != action.save_intent).then(|| { window.prompt( gpui::PromptLevel::Warning, &format!("{path:?} already exists. Do you want to replace it?"), Some( "A file or folder with the same name already exists. Replacing it will overwrite its current contents.", ), &["Replace", "Cancel"], cx ) }); let filename = filename.clone(); cx.spawn_in(window, async move |this, cx| { if let Some(rx) = rx && Ok(0) != rx.await { return; } let _ = this.update_in(cx, |worktree, window, cx| { let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else { return; }; worktree .write_file(path.into_arc(), text.clone(), line_ending, encoding, has_bom, cx) .detach_and_prompt_err("Failed to write lines", window, cx, |_, _, _| None); }); }) .detach(); }); }); }); return; } if action.filename.is_empty() { if let Some(workspace) = vim.workspace(window, cx) { workspace.update(cx, |workspace, cx| { workspace .save_active_item( action.save_intent.unwrap_or(SaveIntent::Save), window, cx, ) .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); }); } return; } vim.update_editor(cx, |_, editor, cx| { let Some(project) = editor.project().cloned() else { return; }; let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else { return; }; let path_style = worktree.read(cx).path_style(); let Ok(project_path) = RelPath::new(Path::new(&action.filename), path_style).map(|path| ProjectPath { worktree_id: worktree.read(cx).id(), path: path.into_arc(), })