From ed23057ff8e01404ab608682445b4f293b6142ed Mon Sep 17 00:00:00 2001 From: Omnikar Date: Sat, 6 Nov 2021 11:57:14 -0400 Subject: Launch with defaults upon invalid config/theme (#982) * Launch with defaults upon invalid config/theme * Startup message if there is a problematic config * Statusline error if trying to switch to an invalid theme * Use serde `deny_unknown_fields` for config--- helix-view/src/editor.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'helix-view') diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 6aa8b04d..17cd3d7b 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -34,7 +34,7 @@ where } #[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(rename_all = "kebab-case", default)] +#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct Config { /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5. pub scrolloff: usize, @@ -195,6 +195,12 @@ impl Editor { } pub fn set_theme(&mut self, theme: Theme) { + // `ui.selection` is the only scope required to be able to render a theme. + if theme.find_scope_index("ui.selection").is_none() { + self.set_error("Invalid theme: `ui.selection` required".to_owned()); + return; + } + let scopes = theme.scopes(); for config in self .syn_loader -- cgit v1.2.3-70-g09d2 From 29e684941389f949491efe4d9807a0edd1facee3 Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Mon, 8 Nov 2021 23:17:54 +0800 Subject: Add LSP rename_symbol (space-r) (#1011) improve apply_workspace_edit--- helix-lsp/src/client.rs | 27 +++++++ helix-term/src/commands.rs | 188 ++++++++++++++++++++++++++++++++++++--------- helix-term/src/keymap.rs | 1 + helix-view/src/editor.rs | 6 ++ 4 files changed, 187 insertions(+), 35 deletions(-) (limited to 'helix-view') diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index b810feef..271fd9d5 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -257,6 +257,12 @@ impl Client { content_format: Some(vec![lsp::MarkupKind::Markdown]), ..Default::default() }), + rename: Some(lsp::RenameClientCapabilities { + dynamic_registration: Some(false), + prepare_support: Some(false), + prepare_support_default_behavior: None, + honors_change_annotations: Some(false), + }), code_action: Some(lsp::CodeActionClientCapabilities { code_action_literal_support: Some(lsp::CodeActionLiteralSupport { code_action_kind: lsp::CodeActionKindLiteralSupport { @@ -773,4 +779,25 @@ impl Client { self.call::(params) } + + pub async fn rename_symbol( + &self, + text_document: lsp::TextDocumentIdentifier, + position: lsp::Position, + new_name: String, + ) -> anyhow::Result { + let params = lsp::RenameParams { + text_document_position: lsp::TextDocumentPositionParams { + text_document, + position, + }, + new_name, + work_done_progress_params: lsp::WorkDoneProgressParams { + work_done_token: None, + }, + }; + + let response = self.request::(params).await?; + Ok(response.unwrap_or_default()) + } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5f091775..245fbe4e 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -23,7 +23,7 @@ use helix_view::{ use anyhow::{anyhow, bail, Context as _}; use helix_lsp::{ - lsp, + block_on, lsp, util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range}, OffsetEncoding, }; @@ -339,6 +339,7 @@ impl Command { shell_append_output, "Append output of shell command after each selection", shell_keep_pipe, "Filter selections with shell predicate", suspend, "Suspend", + rename_symbol, "Rename symbol", ); } @@ -2749,14 +2750,104 @@ pub fn code_action(cx: &mut Context) { ) } +pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> { + use lsp::ResourceOp; + use std::fs; + match op { + ResourceOp::Create(op) => { + let path = op.uri.to_file_path().unwrap(); + let ignore_if_exists = if let Some(options) = &op.options { + !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) + } else { + false + }; + if ignore_if_exists && path.exists() { + Ok(()) + } else { + fs::write(&path, []) + } + } + ResourceOp::Delete(op) => { + let path = op.uri.to_file_path().unwrap(); + if path.is_dir() { + let recursive = if let Some(options) = &op.options { + options.recursive.unwrap_or(false) + } else { + false + }; + if recursive { + fs::remove_dir_all(&path) + } else { + fs::remove_dir(&path) + } + } else if path.is_file() { + fs::remove_file(&path) + } else { + Ok(()) + } + } + ResourceOp::Rename(op) => { + let from = op.old_uri.to_file_path().unwrap(); + let to = op.new_uri.to_file_path().unwrap(); + let ignore_if_exists = if let Some(options) = &op.options { + !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) + } else { + false + }; + if ignore_if_exists && to.exists() { + Ok(()) + } else { + fs::rename(&from, &to) + } + } + } +} + fn apply_workspace_edit( editor: &mut Editor, offset_encoding: OffsetEncoding, workspace_edit: &lsp::WorkspaceEdit, ) { + let mut apply_edits = |uri: &helix_lsp::Url, text_edits: Vec| { + let path = uri + .to_file_path() + .expect("unable to convert URI to filepath"); + + let current_view_id = view!(editor).id; + let doc_id = editor.open(path, Action::Load).unwrap(); + let doc = editor + .document_mut(doc_id) + .expect("Document for document_changes not found"); + + // Need to determine a view for apply/append_changes_to_history + let selections = doc.selections(); + let view_id = if selections.contains_key(¤t_view_id) { + // use current if possible + current_view_id + } else { + // Hack: we take the first available view_id + selections + .keys() + .next() + .copied() + .expect("No view_id available") + }; + + let transaction = helix_lsp::util::generate_transaction_from_edits( + doc.text(), + text_edits, + offset_encoding, + ); + doc.apply(&transaction, view_id); + doc.append_changes_to_history(view_id); + }; + if let Some(ref changes) = workspace_edit.changes { log::debug!("workspace changes: {:?}", changes); - editor.set_error(String::from("Handling workspace_edit.changes is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + for (uri, text_edits) in changes { + let text_edits = text_edits.to_vec(); + apply_edits(uri, text_edits); + } return; // Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used // TODO: find some example that uses workspace changes, and test it @@ -2774,30 +2865,6 @@ fn apply_workspace_edit( match document_changes { lsp::DocumentChanges::Edits(document_edits) => { for document_edit in document_edits { - let path = document_edit - .text_document - .uri - .to_file_path() - .expect("unable to convert URI to filepath"); - let current_view_id = view!(editor).id; - let doc = editor - .document_by_path_mut(path) - .expect("Document for document_changes not found"); - - // Need to determine a view for apply/append_changes_to_history - let selections = doc.selections(); - let view_id = if selections.contains_key(¤t_view_id) { - // use current if possible - current_view_id - } else { - // Hack: we take the first available view_id - selections - .keys() - .next() - .copied() - .expect("No view_id available") - }; - let edits = document_edit .edits .iter() @@ -2809,19 +2876,33 @@ fn apply_workspace_edit( }) .cloned() .collect(); - - let transaction = helix_lsp::util::generate_transaction_from_edits( - doc.text(), - edits, - offset_encoding, - ); - doc.apply(&transaction, view_id); - doc.append_changes_to_history(view_id); + apply_edits(&document_edit.text_document.uri, edits); } } lsp::DocumentChanges::Operations(operations) => { log::debug!("document changes - operations: {:?}", operations); - editor.set_error(String::from("Handling document operations is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + for operateion in operations { + match operateion { + lsp::DocumentChangeOperation::Op(op) => { + apply_document_resource_op(op).unwrap(); + } + + lsp::DocumentChangeOperation::Edit(document_edit) => { + let edits = document_edit + .edits + .iter() + .map(|edit| match edit { + lsp::OneOf::Left(text_edit) => text_edit, + lsp::OneOf::Right(annotated_text_edit) => { + &annotated_text_edit.text_edit + } + }) + .cloned() + .collect(); + apply_edits(&document_edit.text_document.uri, edits); + } + } + } } } } @@ -4982,3 +5063,40 @@ fn add_newline_impl(cx: &mut Context, open: Open) { doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); } + +fn rename_symbol(cx: &mut Context) { + let prompt = Prompt::new( + "Rename to: ".into(), + None, + |_input: &str| Vec::new(), + move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { + if event != PromptEvent::Validate { + return; + } + + log::debug!("renaming to: {:?}", input); + + let (view, doc) = current!(cx.editor); + let language_server = match doc.language_server() { + Some(language_server) => language_server, + None => return, + }; + + let offset_encoding = language_server.offset_encoding(); + + let pos = pos_to_lsp_pos( + doc.text(), + doc.selection(view.id) + .primary() + .cursor(doc.text().slice(..)), + offset_encoding, + ); + + let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string()); + let edits = block_on(task).unwrap_or_default(); + log::debug!("Edits from LSP: {:?}", edits); + apply_workspace_edit(&mut cx.editor, offset_encoding, &edits); + }, + ); + cx.push_layer(Box::new(prompt)); +} diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index ce50f0ab..d497401f 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -584,6 +584,7 @@ impl Default for Keymaps { "R" => replace_selections_with_clipboard, "/" => global_search, "k" => hover, + "r" => rename_symbol, }, "z" => { "View" "z" | "c" => align_view_center, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 17cd3d7b..631dcf0c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -287,6 +287,12 @@ impl Editor { return; } Action::Load => { + let view_id = view!(self).id; + if let Some(doc) = self.document_mut(id) { + if doc.selections().is_empty() { + doc.selections.insert(view_id, Selection::point(0)); + } + } return; } Action::HorizontalSplit => { -- cgit v1.2.3-70-g09d2 From 77dbbc73f9c9b6599bc39b18625285685fe2e4b1 Mon Sep 17 00:00:00 2001 From: ath3 Date: Mon, 8 Nov 2021 16:19:44 +0100 Subject: Detect filetype from shebang line (#1001) --- book/src/guides/adding_languages.md | 3 ++- helix-core/src/indent.rs | 1 + helix-core/src/syntax.rs | 24 ++++++++++++++++++++++++ helix-view/src/document.rs | 4 +++- languages.toml | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) (limited to 'helix-view') diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md index c606f8fc..446eb479 100644 --- a/book/src/guides/adding_languages.md +++ b/book/src/guides/adding_languages.md @@ -33,10 +33,11 @@ These are the available keys and descriptions for the file. | scope | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.` or `text.` in case of markup languages | | injection-regex | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. | | file-types | The filetypes of the language, for example `["yml", "yaml"]` | +| shebangs | The interpreters from the shebang line, for example `["sh", "bash"]` | | roots | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` | | auto-format | Whether to autoformat this language when saving | | comment-token | The token to use as a comment-token | -| indent | The indent to use. Has sub keys `tab-width` and `unit` | +| indent | The indent to use. Has sub keys `tab-width` and `unit` | | config | Language server configuration | ## Queries diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 20f034ea..b6f5081a 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -450,6 +450,7 @@ where language: vec![LanguageConfiguration { scope: "source.rust".to_string(), file_types: vec!["rs".to_string()], + shebangs: vec![], language_id: "Rust".to_string(), highlight_config: OnceCell::new(), config: None, diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index f3e3f238..84952248 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -14,6 +14,8 @@ use std::{ cell::RefCell, collections::{HashMap, HashSet}, fmt, + fs::File, + io::Read, path::Path, sync::Arc, }; @@ -52,6 +54,7 @@ pub struct LanguageConfiguration { pub language_id: String, pub scope: String, // source.rust pub file_types: Vec, // filename ends_with? + pub shebangs: Vec, // interpreter(s) associated with language pub roots: Vec, // these indicate project roots <.git, Cargo.toml> pub comment_token: Option, @@ -254,6 +257,7 @@ pub struct Loader { // highlight_names ? language_configs: Vec>, language_config_ids_by_file_type: HashMap, // Vec + language_config_ids_by_shebang: HashMap, } impl Loader { @@ -261,6 +265,7 @@ impl Loader { let mut loader = Self { language_configs: Vec::new(), language_config_ids_by_file_type: HashMap::new(), + language_config_ids_by_shebang: HashMap::new(), }; for config in config.language { @@ -273,6 +278,11 @@ impl Loader { .language_config_ids_by_file_type .insert(file_type.clone(), language_id); } + for shebang in &config.shebangs { + loader + .language_config_ids_by_shebang + .insert(shebang.clone(), language_id); + } loader.language_configs.push(Arc::new(config)); } @@ -298,6 +308,20 @@ impl Loader { // TODO: content_regex handling conflict resolution } + pub fn language_config_for_shebang(&self, path: &Path) -> Option> { + // Read the first 128 bytes of the file. If its a shebang line, try to find the language + let file = File::open(path).ok()?; + let mut buf = String::with_capacity(128); + file.take(128).read_to_string(&mut buf).ok()?; + static SHEBANG_REGEX: Lazy = + Lazy::new(|| Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+)?)?([^\s\.\d]+)").unwrap()); + let configuration_id = SHEBANG_REGEX + .captures(&buf) + .and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1])); + + configuration_id.and_then(|&id| self.language_configs.get(id).cloned()) + } + pub fn language_config_for_scope(&self, scope: &str) -> Option> { self.language_configs .iter() diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index ce5df8ee..a68ab759 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -494,7 +494,9 @@ impl Document { /// Detect the programming language based on the file type. pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) { if let Some(path) = &self.path { - let language_config = config_loader.language_config_for_file_name(path); + let language_config = config_loader + .language_config_for_file_name(path) + .or_else(|| config_loader.language_config_for_shebang(path)); self.set_language(theme, language_config); } } diff --git a/languages.toml b/languages.toml index 98892171..067138e4 100644 --- a/languages.toml +++ b/languages.toml @@ -3,6 +3,7 @@ name = "rust" scope = "source.rust" injection-regex = "rust" file-types = ["rs"] +shebangs = [] roots = [] auto-format = true comment-token = "//" @@ -17,6 +18,7 @@ name = "toml" scope = "source.toml" injection-regex = "toml" file-types = ["toml"] +shebangs = [] roots = [] comment-token = "#" @@ -27,6 +29,7 @@ name = "protobuf" scope = "source.proto" injection-regex = "protobuf" file-types = ["proto"] +shebangs = [] roots = [] comment-token = "//" @@ -37,6 +40,7 @@ name = "elixir" scope = "source.elixir" injection-regex = "elixir" file-types = ["ex", "exs"] +shebangs = [] roots = [] comment-token = "#" @@ -48,6 +52,7 @@ name = "mint" scope = "source.mint" injection-regex = "mint" file-types = ["mint"] +shebangs = [] roots = [] comment-token = "//" @@ -59,6 +64,7 @@ name = "json" scope = "source.json" injection-regex = "json" file-types = ["json"] +shebangs = [] roots = [] indent = { tab-width = 2, unit = " " } @@ -68,6 +74,7 @@ name = "c" scope = "source.c" injection-regex = "c" file-types = ["c"] # TODO: ["h"] +shebangs = [] roots = [] comment-token = "//" @@ -79,6 +86,7 @@ name = "cpp" scope = "source.cpp" injection-regex = "cpp" file-types = ["cc", "hh", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino"] +shebangs = [] roots = [] comment-token = "//" @@ -90,6 +98,7 @@ name = "c-sharp" scope = "source.csharp" injection-regex = "c-?sharp" file-types = ["cs"] +shebangs = [] roots = [] comment-token = "//" @@ -100,6 +109,7 @@ name = "go" scope = "source.go" injection-regex = "go" file-types = ["go"] +shebangs = [] roots = ["Gopkg.toml", "go.mod"] auto-format = true comment-token = "//" @@ -113,6 +123,7 @@ name = "javascript" scope = "source.js" injection-regex = "^(js|javascript)$" file-types = ["js", "mjs"] +shebangs = [] roots = [] comment-token = "//" # TODO: highlights-jsx, highlights-params @@ -124,6 +135,7 @@ name = "typescript" scope = "source.ts" injection-regex = "^(ts|typescript)$" file-types = ["ts"] +shebangs = [] roots = [] # TODO: highlights-jsx, highlights-params @@ -135,6 +147,7 @@ name = "tsx" scope = "source.tsx" injection-regex = "^(tsx)$" # |typescript file-types = ["tsx"] +shebangs = [] roots = [] # TODO: highlights-jsx, highlights-params @@ -146,6 +159,7 @@ name = "css" scope = "source.css" injection-regex = "css" file-types = ["css"] +shebangs = [] roots = [] indent = { tab-width = 2, unit = " " } @@ -155,6 +169,7 @@ name = "html" scope = "text.html.basic" injection-regex = "html" file-types = ["html"] +shebangs = [] roots = [] indent = { tab-width = 2, unit = " " } @@ -164,6 +179,7 @@ name = "python" scope = "source.python" injection-regex = "python" file-types = ["py"] +shebangs = ["python"] roots = [] comment-token = "#" @@ -176,6 +192,7 @@ name = "nix" scope = "source.nix" injection-regex = "nix" file-types = ["nix"] +shebangs = [] roots = [] comment-token = "#" @@ -187,6 +204,7 @@ name = "ruby" scope = "source.ruby" injection-regex = "ruby" file-types = ["rb"] +shebangs = ["ruby"] roots = [] comment-token = "#" @@ -198,6 +216,7 @@ name = "bash" scope = "source.bash" injection-regex = "bash" file-types = ["sh", "bash"] +shebangs = ["sh", "bash", "dash"] roots = [] comment-token = "#" @@ -209,6 +228,7 @@ name = "php" scope = "source.php" injection-regex = "php" file-types = ["php"] +shebangs = ["php"] roots = [] indent = { tab-width = 4, unit = " " } @@ -218,6 +238,7 @@ name = "latex" scope = "source.tex" injection-regex = "tex" file-types = ["tex"] +shebangs = [] roots = [] comment-token = "%" @@ -228,6 +249,7 @@ name = "julia" scope = "source.julia" injection-regex = "julia" file-types = ["jl"] +shebangs = [] roots = [] comment-token = "#" language-server = { command = "julia", args = [ @@ -253,6 +275,7 @@ name = "java" scope = "source.java" injection-regex = "java" file-types = ["java"] +shebangs = [] roots = [] indent = { tab-width = 4, unit = " " } @@ -261,6 +284,7 @@ name = "ledger" scope = "source.ledger" injection-regex = "ledger" file-types = ["ldg", "ledger", "journal"] +shebangs = [] roots = [] comment-token = ";" indent = { tab-width = 4, unit = " " } @@ -270,6 +294,7 @@ name = "ocaml" scope = "source.ocaml" injection-regex = "ocaml" file-types = ["ml"] +shebangs = [] roots = [] comment-token = "(**)" indent = { tab-width = 2, unit = " " } @@ -278,6 +303,7 @@ indent = { tab-width = 2, unit = " " } name = "ocaml-interface" scope = "source.ocaml.interface" file-types = ["mli"] +shebangs = [] roots = [] comment-token = "(**)" indent = { tab-width = 2, unit = " "} @@ -286,6 +312,7 @@ indent = { tab-width = 2, unit = " "} name = "lua" scope = "source.lua" file-types = ["lua"] +shebangs = [] roots = [] comment-token = "--" indent = { tab-width = 2, unit = " " } @@ -295,6 +322,7 @@ name = "svelte" scope = "source.svelte" injection-regex = "svelte" file-types = ["svelte"] +shebangs = [] roots = [] indent = { tab-width = 2, unit = " " } language-server = { command = "svelteserver", args = ["--stdio"] } @@ -305,6 +333,7 @@ name = "vue" scope = "source.vue" injection-regex = "vue" file-types = ["vue"] +shebangs = [] roots = [] indent = { tab-width = 2, unit = " " } @@ -312,6 +341,7 @@ indent = { tab-width = 2, unit = " " } name = "yaml" scope = "source.yaml" file-types = ["yml", "yaml"] +shebangs = [] roots = [] comment-token = "#" indent = { tab-width = 2, unit = " " } @@ -331,6 +361,7 @@ name = "zig" scope = "source.zig" injection-regex = "zig" file-types = ["zig"] +shebangs = [] roots = ["build.zig"] auto-format = true comment-token = "//" @@ -343,6 +374,7 @@ name = "prolog" scope = "source.prolog" roots = [] file-types = ["pl", "prolog"] +shebangs = ["swipl"] comment-token = "%" language-server = { command = "swipl", args = [ @@ -354,6 +386,7 @@ language-server = { command = "swipl", args = [ name = "tsq" scope = "source.tsq" file-types = ["scm"] +shebangs = [] roots = [] comment-token = ";" indent = { tab-width = 2, unit = " " } @@ -362,6 +395,7 @@ indent = { tab-width = 2, unit = " " } name = "cmake" scope = "source.cmake" file-types = ["cmake", "CMakeLists.txt"] +shebangs = [] roots = [] comment-token = "#" indent = { tab-width = 2, unit = " " } @@ -371,6 +405,7 @@ language-server = { command = "cmake-language-server" } name = "perl" scope = "source.perl" file-types = ["pl", "pm"] +shebangs = ["perl"] roots = [] comment-token = "#" indent = { tab-width = 2, unit = " " } -- cgit v1.2.3-70-g09d2 From 549cdee56159bed4266990ae66591dd6299293d4 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Tue, 9 Nov 2021 00:30:34 +0900 Subject: Refactor shebang detection to reuse the loaded buffer --- helix-core/src/syntax.rs | 11 +++-------- helix-view/src/document.rs | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) (limited to 'helix-view') diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 84952248..0164092d 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -14,8 +14,6 @@ use std::{ cell::RefCell, collections::{HashMap, HashSet}, fmt, - fs::File, - io::Read, path::Path, sync::Arc, }; @@ -308,15 +306,12 @@ impl Loader { // TODO: content_regex handling conflict resolution } - pub fn language_config_for_shebang(&self, path: &Path) -> Option> { - // Read the first 128 bytes of the file. If its a shebang line, try to find the language - let file = File::open(path).ok()?; - let mut buf = String::with_capacity(128); - file.take(128).read_to_string(&mut buf).ok()?; + pub fn language_config_for_shebang(&self, source: &Rope) -> Option> { + let line = Cow::from(source.line(0)); static SHEBANG_REGEX: Lazy = Lazy::new(|| Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+)?)?([^\s\.\d]+)").unwrap()); let configuration_id = SHEBANG_REGEX - .captures(&buf) + .captures(&line) .and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1])); configuration_id.and_then(|&id| self.language_configs.get(id).cloned()) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index a68ab759..351ad05a 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -496,7 +496,7 @@ impl Document { if let Some(path) = &self.path { let language_config = config_loader .language_config_for_file_name(path) - .or_else(|| config_loader.language_config_for_shebang(path)); + .or_else(|| config_loader.language_config_for_shebang(self.text())); self.set_language(theme, language_config); } } -- cgit v1.2.3-70-g09d2 From a69caff450ff8201e16d0a0b4617114e03ed3c97 Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Tue, 9 Nov 2021 10:11:45 +0800 Subject: search_impl will only align cursor center when it isn't in view (#959) --- helix-term/src/commands.rs | 22 +++++++++++++++++++--- helix-view/src/view.rs | 41 +++++++++++++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 11 deletions(-) (limited to 'helix-view') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 245fbe4e..42163b8e 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1175,6 +1175,7 @@ fn search_impl( regex: &Regex, movement: Movement, direction: Direction, + scrolloff: usize, ) { let text = doc.text().slice(..); let selection = doc.selection(view.id); @@ -1233,7 +1234,11 @@ fn search_impl( }; doc.set_selection(view.id, selection); - align_view(doc, view, Align::Center); + if view.is_cursor_in_view(doc, 0) { + view.ensure_cursor_in_view(doc, scrolloff); + } else { + align_view(doc, view, Align::Center) + } }; } @@ -1257,6 +1262,8 @@ fn rsearch(cx: &mut Context) { // TODO: use one function for search vs extend fn searcher(cx: &mut Context, direction: Direction) { let reg = cx.register.unwrap_or('/'); + let scrolloff = cx.editor.config.scrolloff; + let (_, doc) = current!(cx.editor); // TODO: could probably share with select_on_matches? @@ -1281,7 +1288,15 @@ fn searcher(cx: &mut Context, direction: Direction) { if event != PromptEvent::Update { return; } - search_impl(doc, view, &contents, ®ex, Movement::Move, direction); + search_impl( + doc, + view, + &contents, + ®ex, + Movement::Move, + direction, + scrolloff, + ); }, ); @@ -1289,6 +1304,7 @@ fn searcher(cx: &mut Context, direction: Direction) { } fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) { + let scrolloff = cx.editor.config.scrolloff; let (view, doc) = current!(cx.editor); let registers = &cx.editor.registers; if let Some(query) = registers.read('/') { @@ -1303,7 +1319,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir .case_insensitive(case_insensitive) .build() { - search_impl(doc, view, &contents, ®ex, movement, direction); + search_impl(doc, view, &contents, ®ex, movement, direction, scrolloff); } else { // get around warning `mutable_borrow_reservation_conflict` // which will be a hard error in the future diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 11f30155..6a624ded 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -85,7 +85,12 @@ impl View { self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline } - pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) { + // + pub fn offset_coords_to_in_view( + &self, + doc: &Document, + scrolloff: usize, + ) -> Option<(usize, usize)> { let cursor = doc .selection(self.id) .primary() @@ -104,23 +109,43 @@ impl View { let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize; - if line > last_line.saturating_sub(scrolloff) { + let row = if line > last_line.saturating_sub(scrolloff) { // scroll down - self.offset.row += line - (last_line.saturating_sub(scrolloff)); + self.offset.row + line - (last_line.saturating_sub(scrolloff)) } else if line < self.offset.row + scrolloff { // scroll up - self.offset.row = line.saturating_sub(scrolloff); - } + line.saturating_sub(scrolloff) + } else { + self.offset.row + }; - if col > last_col.saturating_sub(scrolloff) { + let col = if col > last_col.saturating_sub(scrolloff) { // scroll right - self.offset.col += col - (last_col.saturating_sub(scrolloff)); + self.offset.col + col - (last_col.saturating_sub(scrolloff)) } else if col < self.offset.col + scrolloff { // scroll left - self.offset.col = col.saturating_sub(scrolloff); + col.saturating_sub(scrolloff) + } else { + self.offset.col + }; + if row == self.offset.row && col == self.offset.col { + None + } else { + Some((row, col)) } } + pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) { + if let Some((row, col)) = self.offset_coords_to_in_view(doc, scrolloff) { + self.offset.row = row; + self.offset.col = col; + } + } + + pub fn is_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) -> bool { + self.offset_coords_to_in_view(doc, scrolloff).is_none() + } + /// Calculates the last visible line on screen #[inline] pub fn last_line(&self, doc: &Document) -> usize { -- cgit v1.2.3-70-g09d2 From cf831b1a65625f29d6e1bc12483a45c1adc8dff4 Mon Sep 17 00:00:00 2001 From: Jason Hansen Date: Tue, 9 Nov 2021 18:53:14 -0700 Subject: Allow piping from stdin into a buffer on startup (#996) * Allow piping from stdin into a buffer on startup * Refactor * Don't allow piping into new buffer on macOS * Update helix-term/src/application.rs Co-authored-by: Blaž Hrastnik * Update helix-term/src/application.rs Co-authored-by: Blaž Hrastnik * Fix Co-authored-by: Blaž Hrastnik --- helix-term/src/application.rs | 14 ++++++++++++-- helix-view/src/editor.rs | 17 +++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) (limited to 'helix-view') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index f1884199..b04eef0d 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -7,7 +7,7 @@ use crate::{args::Args, compositor::Compositor, config::Config, job::Jobs, ui}; use log::{error, warn}; use std::{ - io::{stdout, Write}, + io::{stdin, stdout, Write}, sync::Arc, time::{Duration, Instant}, }; @@ -17,6 +17,7 @@ use anyhow::Error; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream}, execute, terminal, + tty::IsTty, }; #[cfg(not(windows))] use { @@ -134,8 +135,17 @@ impl Application { } editor.set_status(format!("Loaded {} files.", nr_of_files)); } - } else { + } else if stdin().is_tty() { editor.new_file(Action::VerticalSplit); + } else if cfg!(target_os = "macos") { + // On Linux and Windows, we allow the output of a command to be piped into the new buffer. + // This doesn't currently work on macOS because of the following issue: + // https://github.com/crossterm-rs/crossterm/issues/500 + anyhow::bail!("Piping into helix-term is currently not supported on macOS"); + } else { + editor + .new_file_from_stdin(Action::VerticalSplit) + .unwrap_or_else(|_| editor.new_file(Action::VerticalSplit)); } editor.set_theme(theme); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 631dcf0c..7650d217 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -9,6 +9,7 @@ use crate::{ use futures_util::future; use std::{ collections::BTreeMap, + io::stdin, path::{Path, PathBuf}, pin::Pin, sync::Arc, @@ -314,16 +315,24 @@ impl Editor { self._refresh(); } - pub fn new_file(&mut self, action: Action) -> DocumentId { + fn new_file_from_document(&mut self, action: Action, mut document: Document) -> DocumentId { let id = DocumentId(self.next_document_id); self.next_document_id += 1; - let mut doc = Document::default(); - doc.id = id; - self.documents.insert(id, doc); + document.id = id; + self.documents.insert(id, document); self.switch(id, action); id } + pub fn new_file(&mut self, action: Action) -> DocumentId { + self.new_file_from_document(action, Document::default()) + } + + pub fn new_file_from_stdin(&mut self, action: Action) -> Result { + let (rope, encoding) = crate::document::from_reader(&mut stdin(), None)?; + Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding)))) + } + pub fn open(&mut self, path: PathBuf, action: Action) -> Result { let path = helix_core::path::get_canonicalized_path(&path)?; -- cgit v1.2.3-70-g09d2 From 9d591427be900b7a43fc7e13dd86f31199e8c00e Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Thu, 11 Nov 2021 21:32:44 +0800 Subject: Fix earlier/later missing changeset update (#1069) Fix #1059--- helix-term/src/commands.rs | 10 ++++++++-- helix-view/src/document.rs | 24 ++++++++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) (limited to 'helix-view') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 23b385eb..738621b0 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1845,7 +1845,10 @@ mod cmd { .map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); - doc.earlier(view.id, uk); + let success = doc.earlier(view.id, uk); + if !success { + cx.editor.set_status("Already at oldest change".to_owned()); + } Ok(()) } @@ -1860,7 +1863,10 @@ mod cmd { .parse::() .map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); - doc.later(view.id, uk); + let success = doc.later(view.id, uk); + if !success { + cx.editor.set_status("Already at newest change".to_owned()); + } Ok(()) } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 351ad05a..80f6a740 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -751,19 +751,35 @@ impl Document { } /// Undo modifications to the [`Document`] according to `uk`. - pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) { + pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool { let txns = self.history.get_mut().earlier(uk); + let mut success = false; for txn in txns { - self.apply_impl(&txn, view_id); + if self.apply_impl(&txn, view_id) { + success = true; + } + } + if success { + // reset changeset to fix len + self.changes = ChangeSet::new(self.text()); } + success } /// Redo modifications to the [`Document`] according to `uk`. - pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) { + pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool { let txns = self.history.get_mut().later(uk); + let mut success = false; for txn in txns { - self.apply_impl(&txn, view_id); + if self.apply_impl(&txn, view_id) { + success = true; + } + } + if success { + // reset changeset to fix len + self.changes = ChangeSet::new(self.text()); } + success } /// Commit pending changes to history -- cgit v1.2.3-70-g09d2 From b824e091a948c076a428fb981cd5be2929378533 Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Fri, 12 Nov 2021 20:15:41 -0800 Subject: helix-term/commands: move SCRATCH_BUFFER_NAME to helix-view/document (#1091) This way, the name is accessible everywhere `Document` and related types are.--- helix-term/src/commands.rs | 4 +--- helix-view/src/document.rs | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'helix-view') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 48fd0ee0..d5a48c5f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -13,7 +13,7 @@ use helix_core::{ use helix_view::{ clipboard::ClipboardType, - document::Mode, + document::{Mode, SCRATCH_BUFFER_NAME}, editor::{Action, Motion}, input::KeyEvent, keyboard::KeyCode, @@ -53,8 +53,6 @@ use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use ignore::{DirEntry, WalkBuilder, WalkState}; use tokio_stream::wrappers::UnboundedReceiverStream; -pub const SCRATCH_BUFFER_NAME: &str = "[scratch]"; - pub struct Context<'a> { pub register: Option, pub count: Option, diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 80f6a740..6b429151 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -25,6 +25,8 @@ const BUF_SIZE: usize = 8192; const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4); +pub const SCRATCH_BUFFER_NAME: &str = "[scratch]"; + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Mode { Normal, -- cgit v1.2.3-70-g09d2 From 35c974c9c49f9127da3798c9a8e49795b3c4aadc Mon Sep 17 00:00:00 2001 From: ath3 Date: Sun, 14 Nov 2021 16:11:53 +0100 Subject: Implement "Goto last modification" command (#1067) --- book/src/keymap.md | 1 + helix-core/src/history.rs | 30 +++++++++++++++++++++++++++++- helix-term/src/commands.rs | 14 ++++++++++++++ helix-term/src/keymap.rs | 1 + helix-term/src/ui/editor.rs | 2 +- helix-view/src/document.rs | 2 +- 6 files changed, 47 insertions(+), 3 deletions(-) (limited to 'helix-view') diff --git a/book/src/keymap.md b/book/src/keymap.md index c544a472..6155e553 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -165,6 +165,7 @@ Jumps to various locations. | `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` | | `n` | Go to next buffer | `goto_next_buffer` | | `p` | Go to previous buffer | `goto_previous_buffer` | +| `.` | Go to last modification in current file | `goto_last_modification` | #### Match mode diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index b53c01fe..bf2624e2 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -1,4 +1,4 @@ -use crate::{ChangeSet, Rope, State, Transaction}; +use crate::{Assoc, ChangeSet, Rope, State, Transaction}; use once_cell::sync::Lazy; use regex::Regex; use std::num::NonZeroUsize; @@ -133,6 +133,34 @@ impl History { Some(&self.revisions[last_child.get()].transaction) } + // Get the position of last change + pub fn last_edit_pos(&self) -> Option { + if self.current == 0 { + return None; + } + let current_revision = &self.revisions[self.current]; + let primary_selection = current_revision + .inversion + .selection() + .expect("inversion always contains a selection") + .primary(); + let (_from, to, _fragment) = current_revision + .transaction + .changes_iter() + // find a change that matches the primary selection + .find(|(from, to, _fragment)| { + crate::Range::new(*from, *to).overlaps(&primary_selection) + }) + // or use the first change + .or_else(|| current_revision.transaction.changes_iter().next()) + .unwrap(); + let pos = current_revision + .transaction + .changes() + .map_pos(to, Assoc::After); + Some(pos) + } + fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize { use std::collections::HashSet; let mut a_path_set = HashSet::new(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d5a48c5f..e37265a8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -257,6 +257,7 @@ impl Command { goto_window_middle, "Goto window middle", goto_window_bottom, "Goto window bottom", goto_last_accessed_file, "Goto last accessed file", + goto_last_modification, "Goto last modification", goto_line, "Goto line", goto_last_line, "Goto last line", goto_first_diag, "Goto first diagnostic", @@ -3195,6 +3196,19 @@ fn goto_last_accessed_file(cx: &mut Context) { } } +fn goto_last_modification(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + let pos = doc.history.get_mut().last_edit_pos(); + let text = doc.text().slice(..); + if let Some(pos) = pos { + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); + } +} + fn select_mode(cx: &mut Context) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index e3e01995..b14b1a6f 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -525,6 +525,7 @@ impl Default for Keymaps { "a" => goto_last_accessed_file, "n" => goto_next_buffer, "p" => goto_previous_buffer, + "." => goto_last_modification, }, ":" => command_mode, diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index dcf87203..bcd9f8f0 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -743,7 +743,7 @@ impl EditorView { std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i)); } // special handling for repeat operator - key!('.') => { + key!('.') if self.keymaps.pending().is_empty() => { // first execute whatever put us into insert mode self.last_insert.0.execute(cxt); // then replay the inputs diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 6b429151..76b19a07 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -98,7 +98,7 @@ pub struct Document { // It can be used as a cell where we will take it out to get some parts of the history and put // it back as it separated from the edits. We could split out the parts manually but that will // be more troublesome. - history: Cell, + pub history: Cell, pub savepoint: Option, -- cgit v1.2.3-70-g09d2 From 87e61a0894d6af1838c5d288fae83279004026fb Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Sun, 14 Nov 2021 20:06:12 -0800 Subject: helix-term/commands: implement cquit (#1096) This allows you to exit helix with an exit code, e.g. `:cq 2`.--- helix-term/src/application.rs | 4 ++-- helix-term/src/commands.rs | 26 ++++++++++++++++++++++++++ helix-term/src/main.rs | 12 +++++++++--- helix-view/src/editor.rs | 3 +++ 4 files changed, 40 insertions(+), 5 deletions(-) (limited to 'helix-view') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index b04eef0d..2969a9e5 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -599,7 +599,7 @@ impl Application { Ok(()) } - pub async fn run(&mut self) -> Result<(), Error> { + pub async fn run(&mut self) -> Result { self.claim_term().await?; // Exit the alternate screen and disable raw mode before panicking @@ -622,6 +622,6 @@ impl Application { self.restore_term()?; - Ok(()) + Ok(self.editor.exit_code) } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ab62c1aa..8c0a005c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2042,6 +2042,25 @@ mod cmd { quit_all_impl(&mut cx.editor, args, event, true) } + fn cquit( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let exit_code = args + .first() + .and_then(|code| code.parse::().ok()) + .unwrap_or(1); + cx.editor.exit_code = exit_code; + + let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect(); + for view_id in views { + cx.editor.close(view_id, false); + } + + Ok(()) + } + fn theme( cx: &mut compositor::Context, args: &[&str], @@ -2411,6 +2430,13 @@ mod cmd { fun: force_quit_all, completer: None, }, + TypableCommand { + name: "cquit", + aliases: &["cq"], + doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).", + fun: cquit, + completer: None, + }, TypableCommand { name: "theme", aliases: &[], diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 3ae4f7c9..6fa1ce67 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -38,8 +38,13 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { Ok(()) } +fn main() -> Result<()> { + let exit_code = main_impl()?; + std::process::exit(exit_code); +} + #[tokio::main] -async fn main() -> Result<()> { +async fn main_impl() -> Result { let cache_dir = helix_core::cache_dir(); if !cache_dir.exists() { std::fs::create_dir_all(&cache_dir).ok(); @@ -109,7 +114,8 @@ FLAGS: // TODO: use the thread local executor to spawn the application task separately from the work pool let mut app = Application::new(args, config).context("unable to create new application")?; - app.run().await.unwrap(); - Ok(()) + let exit_code = app.run().await?; + + Ok(exit_code) } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 7650d217..e4015707 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -129,6 +129,8 @@ pub struct Editor { pub idle_timer: Pin>, pub last_motion: Option, + + pub exit_code: i32, } #[derive(Debug, Copy, Clone)] @@ -167,6 +169,7 @@ impl Editor { idle_timer: Box::pin(sleep(config.idle_timeout)), last_motion: None, config, + exit_code: 0, } } -- cgit v1.2.3-70-g09d2 From c638b6b60e69697b7e7957ed1af1ac071c41974b Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Mon, 15 Nov 2021 07:30:45 -0800 Subject: helix-term/commands: implement buffer-close (bc, bclose) (#1035) * helix-view/view: impl method to remove document from jumps * helix-view/editor: impl close_document * helix-view/editor: remove close_buffer argument from `close` According to archseer, this was never implemented or used properly. Now that we have a proper "buffer close" function, we can get rid of this. * helix-term/commands: implement buffer-close (bc, bclose) This behaves the same as Kakoune's `delete-buffer` / `db` command: * With 3 files opened by the user with `:o ab`, `:o cd`, and `:o ef`: * `buffer-close` once closes `ef` and switches to `cd` * `buffer-close` again closes `cd` and switches to `ab` * `buffer-close` again closes `ab` and switches to a scratch buffer * With 3 files opened from the command line with `hx -- ab cd ef`: * `buffer-close` once closes `ab` and switches to `cd` * `buffer-close` again closes `cd` and switches to `ef` * `buffer-close` again closes `ef` and switches to a scratch buffer * With 1 file opened (`ab`): * `buffer-close` once closes `ab` and switches to a scratch buffer * `buffer-close` again closes the scratch buffer and switches to a new scratch buffer * helix-term/commands: implement buffer-close! (bclose!, bc!) Namely, if you have a document open in multiple splits, all the splits will be closed at the same time, leaving only splits without that document focused (or a scratch buffer if they were all focused on that buffer). * helix-view/tree: reset focus if Tree is empty --- helix-term/src/commands.rs | 50 ++++++++++++++++---- helix-view/src/editor.rs | 113 ++++++++++++++++++++++++++++++++++----------- helix-view/src/tree.rs | 3 ++ helix-view/src/view.rs | 4 ++ 4 files changed, 135 insertions(+), 35 deletions(-) (limited to 'helix-view') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 8c0a005c..c7aab726 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1700,8 +1700,7 @@ mod cmd { buffers_remaining_impl(cx.editor)? } - cx.editor - .close(view!(cx.editor).id, /* close_buffer */ false); + cx.editor.close(view!(cx.editor).id); Ok(()) } @@ -1711,8 +1710,7 @@ mod cmd { _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - cx.editor - .close(view!(cx.editor).id, /* close_buffer */ false); + cx.editor.close(view!(cx.editor).id); Ok(()) } @@ -1730,6 +1728,28 @@ mod cmd { Ok(()) } + fn buffer_close( + cx: &mut compositor::Context, + _args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let view = view!(cx.editor); + let doc_id = view.doc; + cx.editor.close_document(doc_id, false)?; + Ok(()) + } + + fn force_buffer_close( + cx: &mut compositor::Context, + _args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let view = view!(cx.editor); + let doc_id = view.doc; + cx.editor.close_document(doc_id, true)?; + Ok(()) + } + fn write_impl>( cx: &mut compositor::Context, path: Option

, @@ -1976,7 +1996,7 @@ mod cmd { // close all views let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect(); for view_id in views { - cx.editor.close(view_id, false); + cx.editor.close(view_id); } } @@ -2020,7 +2040,7 @@ mod cmd { // close all views let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect(); for view_id in views { - editor.close(view_id, false); + editor.close(view_id); } Ok(()) @@ -2332,6 +2352,20 @@ mod cmd { fun: open, completer: Some(completers::filename), }, + TypableCommand { + name: "buffer-close", + aliases: &["bc", "bclose"], + doc: "Close the current buffer.", + fun: buffer_close, + completer: None, // FIXME: buffer completer + }, + TypableCommand { + name: "buffer-close!", + aliases: &["bc!", "bclose!"], + doc: "Close the current buffer forcefully (ignoring unsaved changes).", + fun: force_buffer_close, + completer: None, // FIXME: buffer completer + }, TypableCommand { name: "write", aliases: &["w"], @@ -4914,7 +4948,7 @@ fn wclose(cx: &mut Context) { } let view_id = view!(cx.editor).id; // close current split - cx.editor.close(view_id, /* close_buffer */ false); + cx.editor.close(view_id); } fn wonly(cx: &mut Context) { @@ -4926,7 +4960,7 @@ fn wonly(cx: &mut Context) { .collect::>(); for (view_id, focus) in views { if !focus { - cx.editor.close(view_id, /* close_buffer */ false); + cx.editor.close(view_id); } } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index e4015707..4712c52a 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -22,7 +22,7 @@ use anyhow::Error; pub use helix_core::diagnostic::Severity; pub use helix_core::register::Registers; use helix_core::syntax; -use helix_core::Position; +use helix_core::{Position, Selection}; use serde::Deserialize; @@ -235,9 +235,28 @@ impl Editor { } } + fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) { + let view = self.tree.get_mut(current_view); + view.doc = doc_id; + view.offset = Position::default(); + + let doc = self.documents.get_mut(&doc_id).unwrap(); + + // initialize selection for view + doc.selections + .entry(view.id) + .or_insert_with(|| Selection::point(0)); + // TODO: reuse align_view + let pos = doc + .selection(view.id) + .primary() + .cursor(doc.text().slice(..)); + let line = doc.text().char_to_line(pos); + view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2); + } + pub fn switch(&mut self, id: DocumentId, action: Action) { use crate::tree::Layout; - use helix_core::Selection; if !self.documents.contains_key(&id) { log::error!("cannot switch to document that does not exist (anymore)"); @@ -271,22 +290,9 @@ impl Editor { view.jumps.push(jump); view.last_accessed_doc = Some(view.doc); } - view.doc = id; - view.offset = Position::default(); - - let (view, doc) = current!(self); - // initialize selection for view - doc.selections - .entry(view.id) - .or_insert_with(|| Selection::point(0)); - // TODO: reuse align_view - let pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - let line = doc.text().char_to_line(pos); - view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2); + let view_id = view.id; + self.replace_document_in_view(view_id, id); return; } @@ -318,11 +324,16 @@ impl Editor { self._refresh(); } - fn new_file_from_document(&mut self, action: Action, mut document: Document) -> DocumentId { + fn new_document(&mut self, mut document: Document) -> DocumentId { let id = DocumentId(self.next_document_id); self.next_document_id += 1; document.id = id; self.documents.insert(id, document); + id + } + + fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId { + let id = self.new_document(document); self.switch(id, action); id } @@ -392,7 +403,7 @@ impl Editor { Ok(id) } - pub fn close(&mut self, id: ViewId, close_buffer: bool) { + pub fn close(&mut self, id: ViewId) { let view = self.tree.get(self.tree.focus); // remove selection self.documents @@ -401,18 +412,66 @@ impl Editor { .selections .remove(&id); - if close_buffer { - // get around borrowck issues - let doc = &self.documents[&view.doc]; + self.tree.remove(id); + self._refresh(); + } - if let Some(language_server) = doc.language_server() { - tokio::spawn(language_server.text_document_did_close(doc.identifier())); - } - self.documents.remove(&view.doc); + pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> { + let doc = match self.documents.get(&doc_id) { + Some(doc) => doc, + None => anyhow::bail!("document does not exist"), + }; + + if !force && doc.is_modified() { + anyhow::bail!( + "buffer {:?} is modified", + doc.relative_path() + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_else(|| "[scratch]".into()) + ); + } + + if let Some(language_server) = doc.language_server() { + tokio::spawn(language_server.text_document_did_close(doc.identifier())); + } + + let views_to_close = self + .tree + .views() + .filter_map(|(view, _focus)| { + if view.doc == doc_id { + Some(view.id) + } else { + None + } + }) + .collect::>(); + + for view_id in views_to_close { + self.close(view_id); + } + + self.documents.remove(&doc_id); + + // If the document we removed was visible in all views, we will have no more views. We don't + // want to close the editor just for a simple buffer close, so we need to create a new view + // containing either an existing document, or a brand new document. + if self.tree.views().peekable().peek().is_none() { + let doc_id = self + .documents + .iter() + .map(|(&doc_id, _)| doc_id) + .next() + .unwrap_or_else(|| self.new_document(Document::default())); + let view = View::new(doc_id); + let view_id = self.tree.insert(view); + let doc = self.documents.get_mut(&doc_id).unwrap(); + doc.selections.insert(view_id, Selection::point(0)); } - self.tree.remove(id); self._refresh(); + + Ok(()) } pub fn resize(&mut self, area: Rect) { diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index 064334b1..de5046ac 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -314,6 +314,9 @@ impl Tree { pub fn recalculate(&mut self) { if self.is_empty() { + // There are no more views, so the tree should focus itself again. + self.focus = self.root; + return; } diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 6a624ded..a77f1562 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -54,6 +54,10 @@ impl JumpList { None } } + + pub fn remove(&mut self, doc_id: &DocumentId) { + self.jumps.retain(|(other_id, _)| other_id != doc_id); + } } #[derive(Debug)] -- cgit v1.2.3-70-g09d2 From 225e7904ec4864b42d0a79f99ebad06ed681c929 Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Mon, 15 Nov 2021 08:46:39 -0800 Subject: helix-view/editor: use SCRATCH_BUFFER_NAME const (#1104) --- helix-view/src/editor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'helix-view') diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 4712c52a..364865d9 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,5 +1,6 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, + document::SCRATCH_BUFFER_NAME, graphics::{CursorKind, Rect}, theme::{self, Theme}, tree::{self, Tree}, @@ -427,7 +428,7 @@ impl Editor { "buffer {:?} is modified", doc.relative_path() .map(|path| path.to_string_lossy().to_string()) - .unwrap_or_else(|| "[scratch]".into()) + .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()) ); } -- cgit v1.2.3-70-g09d2 From 27ceeb83bb055c90670cb9a4d8fdab7d5c742b2f Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 18 Nov 2021 14:13:42 +0900 Subject: Simplify view/doc macros --- helix-view/src/macros.rs | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) (limited to 'helix-view') diff --git a/helix-view/src/macros.rs b/helix-view/src/macros.rs index 63d76a42..04f8df94 100644 --- a/helix-view/src/macros.rs +++ b/helix-view/src/macros.rs @@ -11,10 +11,19 @@ /// Returns `(&mut View, &mut Document)` #[macro_export] macro_rules! current { - ( $( $editor:ident ).+ ) => {{ - let view = $crate::view_mut!( $( $editor ).+ ); + ($editor:expr) => {{ + let view = $crate::view_mut!($editor); let id = view.doc; - let doc = $( $editor ).+ .documents.get_mut(&id).unwrap(); + let doc = $editor.documents.get_mut(&id).unwrap(); + (view, doc) + }}; +} + +#[macro_export] +macro_rules! current_ref { + ($editor:expr) => {{ + let view = $editor.tree.get($editor.tree.focus); + let doc = &$editor.documents[&view.doc]; (view, doc) }}; } @@ -23,8 +32,8 @@ macro_rules! current { /// Returns `&mut Document` #[macro_export] macro_rules! doc_mut { - ( $( $editor:ident ).+ ) => {{ - $crate::current!( $( $editor ).+ ).1 + ($editor:expr) => {{ + $crate::current!($editor).1 }}; } @@ -32,8 +41,8 @@ macro_rules! doc_mut { /// Returns `&mut View` #[macro_export] macro_rules! view_mut { - ( $( $editor:ident ).+ ) => {{ - $( $editor ).+ .tree.get_mut($( $editor ).+ .tree.focus) + ($editor:expr) => {{ + $editor.tree.get_mut($editor.tree.focus) }}; } @@ -41,23 +50,14 @@ macro_rules! view_mut { /// Returns `&View` #[macro_export] macro_rules! view { - ( $( $editor:ident ).+ ) => {{ - $( $editor ).+ .tree.get($( $editor ).+ .tree.focus) + ($editor:expr) => {{ + $editor.tree.get($editor.tree.focus) }}; } #[macro_export] macro_rules! doc { - ( $( $editor:ident ).+ ) => {{ - $crate::current_ref!( $( $editor ).+ ).1 - }}; -} - -#[macro_export] -macro_rules! current_ref { - ( $( $editor:ident ).+ ) => {{ - let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus); - let doc = &$( $editor ).+ .documents[&view.doc]; - (view, doc) + ($editor:expr) => {{ + $crate::current_ref!($editor).1 }}; } -- cgit v1.2.3-70-g09d2