From 4304b52ff86f829115cb89b20160b4f80dbb3dbd Mon Sep 17 00:00:00 2001 From: Carter Snook Date: Sun, 7 Nov 2021 18:50:03 -0600 Subject: fix(core): stop merging array toml config values (#1004) --- helix-core/src/lib.rs | 55 ++++++++++++++++++++------------------------------- 1 file changed, 21 insertions(+), 34 deletions(-) (limited to 'helix-core') diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index d1720df0..6168d02c 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -103,25 +103,7 @@ pub fn cache_dir() -> std::path::PathBuf { pub fn merge_toml_values(left: toml::Value, right: toml::Value) -> toml::Value { use toml::Value; - fn get_name(v: &Value) -> Option<&str> { - v.get("name").and_then(Value::as_str) - } - match (left, right) { - (Value::Array(mut left_items), Value::Array(right_items)) => { - left_items.reserve(right_items.len()); - for rvalue in right_items { - let lvalue = get_name(&rvalue) - .and_then(|rname| left_items.iter().position(|v| get_name(v) == Some(rname))) - .map(|lpos| left_items.remove(lpos)); - let mvalue = match lvalue { - Some(lvalue) => merge_toml_values(lvalue, rvalue), - None => rvalue, - }; - left_items.push(mvalue); - } - Value::Array(left_items) - } (Value::Table(mut left_map), Value::Table(right_map)) => { for (rname, rvalue) in right_map { match left_map.remove(&rname) { @@ -149,12 +131,13 @@ mod merge_toml_tests { fn language_tomls() { use toml::Value; - const USER: &str = " + const USER: &str = r#" [[language]] - name = \"nix\" - test = \"bbb\" - indent = { tab-width = 4, unit = \" \", test = \"aaa\" } - "; + name = "typescript" + test = "bbb" + indent = { tab-width = 4, unit = " ", test = "aaa" } + language-server = { command = "deno", args = ["lsp"] } + "#; let base: Value = toml::from_slice(include_bytes!("../../languages.toml")) .expect("Couldn't parse built-in langauges config"); @@ -162,23 +145,27 @@ mod merge_toml_tests { let merged = merge_toml_values(base, user); let languages = merged.get("language").unwrap().as_array().unwrap(); - let nix = languages + let ts = languages .iter() - .find(|v| v.get("name").unwrap().as_str().unwrap() == "nix") + .find(|v| v.get("name").unwrap().as_str().unwrap() == "typescript") .unwrap(); - let nix_indent = nix.get("indent").unwrap(); + let ts_indent = ts.get("indent").unwrap(); // We changed tab-width and unit in indent so check them if they are the new values + assert_eq!(ts_indent.get("tab-width").unwrap().as_integer().unwrap(), 4); + assert_eq!(ts_indent.get("unit").unwrap().as_str().unwrap(), " "); + // We added a new keys, so check them + assert_eq!(ts.get("test").unwrap().as_str().unwrap(), "bbb"); + assert_eq!(ts_indent.get("test").unwrap().as_str().unwrap(), "aaa"); assert_eq!( - nix_indent.get("tab-width").unwrap().as_integer().unwrap(), - 4 + ts.get("language-server") + .unwrap() + .get("args") + .unwrap() + .as_array() + .unwrap(), + &vec![Value::String("lsp".into())], ); - assert_eq!(nix_indent.get("unit").unwrap().as_str().unwrap(), " "); - // We added a new keys, so check them - assert_eq!(nix.get("test").unwrap().as_str().unwrap(), "bbb"); - assert_eq!(nix_indent.get("test").unwrap().as_str().unwrap(), "aaa"); - // We didn't change comment-token so it should be same - assert_eq!(nix.get("comment-token").unwrap().as_str().unwrap(), "#"); } } -- cgit v1.2.3-70-g09d2 From a252ecd8c85af5cc16638a4752011e2e920fa652 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Sun, 7 Nov 2021 19:54:39 -0500 Subject: Add WORD textobject (#991) * Add WORD textobject * Document WORD textobject--- book/src/usage.md | 1 + helix-core/src/textobject.rs | 11 ++++++----- helix-term/src/commands.rs | 3 ++- helix-term/src/ui/prompt.rs | 1 + 4 files changed, 10 insertions(+), 6 deletions(-) (limited to 'helix-core') diff --git a/book/src/usage.md b/book/src/usage.md index 71730fa8..6b7cbc41 100644 --- a/book/src/usage.md +++ b/book/src/usage.md @@ -62,6 +62,7 @@ Currently supported: `word`, `surround`, `function`, `class`, `parameter`. | Key after `mi` or `ma` | Textobject selected | | --- | --- | | `w` | Word | +| `W` | WORD | | `(`, `[`, `'`, etc | Specified surround pairs | | `f` | Function | | `c` | Class | diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index 975ed115..24f063d4 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -10,7 +10,7 @@ use crate::surround; use crate::syntax::LanguageConfiguration; use crate::Range; -fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize { +fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize { use CharCategory::{Eol, Whitespace}; let iter = match direction { @@ -33,7 +33,7 @@ fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> match categorize_char(ch) { Eol | Whitespace => return pos, category => { - if category != prev_category && pos != 0 && pos != slice.len_chars() { + if !long && category != prev_category && pos != 0 && pos != slice.len_chars() { return pos; } else { match direction { @@ -70,13 +70,14 @@ pub fn textobject_word( range: Range, textobject: TextObject, _count: usize, + long: bool, ) -> Range { let pos = range.cursor(slice); - let word_start = find_word_boundary(slice, pos, Direction::Backward); + let word_start = find_word_boundary(slice, pos, Direction::Backward, long); let word_end = match slice.get_char(pos).map(categorize_char) { None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos, - _ => find_word_boundary(slice, pos + 1, Direction::Forward), + _ => find_word_boundary(slice, pos + 1, Direction::Forward, long), }; // Special case. @@ -268,7 +269,7 @@ mod test { let slice = doc.slice(..); for &case in scenario { let (pos, objtype, expected_range) = case; - let result = textobject_word(slice, Range::point(pos), objtype, 1); + let result = textobject_word(slice, Range::point(pos), objtype, 1, false); assert_eq!( result, expected_range.into(), diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 80cbd6d2..5f091775 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4672,7 +4672,8 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { let selection = doc.selection(view.id).clone().transform(|range| { match ch { - 'w' => textobject::textobject_word(text, range, objtype, count), + 'w' => textobject::textobject_word(text, range, objtype, count, false), + 'W' => textobject::textobject_word(text, range, objtype, count, true), 'c' => textobject_treesitter("class", range), 'f' => textobject_treesitter("function", range), 'p' => textobject_treesitter("parameter", range), diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index c999ba14..29ca18b1 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -491,6 +491,7 @@ impl Component for Prompt { doc.selection(view.id).primary(), textobject::TextObject::Inside, 1, + false, ); let line = text.slice(range.from()..range.to()).to_string(); if !line.is_empty() { -- cgit v1.2.3-70-g09d2 From 1e793c2bbfb4e6d3226f4ba97d99a70934b5cf5c Mon Sep 17 00:00:00 2001 From: Daniel S Poulin Date: Sun, 7 Nov 2021 19:57:26 -0500 Subject: Adds single and double quotes to matching pairs (#995) This enables `mm` to work on quote characters as well as highlighting of matching quote when on it.--- helix-core/src/match_brackets.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'helix-core') diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs index a4b2fb9c..136ce320 100644 --- a/helix-core/src/match_brackets.rs +++ b/helix-core/src/match_brackets.rs @@ -1,6 +1,13 @@ use crate::{Rope, Syntax}; -const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']'), ('<', '>')]; +const PAIRS: &[(char, char)] = &[ + ('(', ')'), + ('{', '}'), + ('[', ']'), + ('<', '>'), + ('\'', '\''), + ('"', '"'), +]; // limit matching pairs to only ( ) { } [ ] < > #[must_use] -- cgit v1.2.3-70-g09d2 From 82ff5b0ab6fd620a23744ffa514ce7a82900ca02 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sat, 6 Nov 2021 23:57:42 +0900 Subject: Specify capacity on toggle_line_comments --- helix-core/src/comment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'helix-core') diff --git a/helix-core/src/comment.rs b/helix-core/src/comment.rs index 4072a532..b22a95a6 100644 --- a/helix-core/src/comment.rs +++ b/helix-core/src/comment.rs @@ -63,7 +63,7 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st let token = token.unwrap_or("//"); let comment = Tendril::from(format!("{} ", token)); - let mut lines: Vec = Vec::new(); + let mut lines: Vec = Vec::with_capacity(selection.len()); let mut min_next_line = 0; for selection in selection { -- 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-core') 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-core') 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 f804ed3192600fcfd704f3613edde37bbba26050 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Tue, 9 Nov 2021 10:57:08 +0900 Subject: Make shebangs optional, they don't make sense outside of scripts --- helix-core/src/syntax.rs | 3 ++- languages.toml | 28 +++------------------------- 2 files changed, 5 insertions(+), 26 deletions(-) (limited to 'helix-core') diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 0164092d..a5dd0c59 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -52,7 +52,8 @@ 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 + #[serde(default)] + pub shebangs: Vec, // interpreter(s) associated with language pub roots: Vec, // these indicate project roots <.git, Cargo.toml> pub comment_token: Option, diff --git a/languages.toml b/languages.toml index 934def44..2ce9321e 100644 --- a/languages.toml +++ b/languages.toml @@ -3,7 +3,6 @@ name = "rust" scope = "source.rust" injection-regex = "rust" file-types = ["rs"] -shebangs = [] roots = [] auto-format = true comment-token = "//" @@ -18,7 +17,6 @@ name = "toml" scope = "source.toml" injection-regex = "toml" file-types = ["toml"] -shebangs = [] roots = [] comment-token = "#" @@ -29,7 +27,6 @@ name = "protobuf" scope = "source.proto" injection-regex = "protobuf" file-types = ["proto"] -shebangs = [] roots = [] comment-token = "//" @@ -40,7 +37,7 @@ name = "elixir" scope = "source.elixir" injection-regex = "elixir" file-types = ["ex", "exs"] -shebangs = [] +shebangs = ["elixir"] roots = [] comment-token = "#" @@ -64,7 +61,6 @@ name = "json" scope = "source.json" injection-regex = "json" file-types = ["json"] -shebangs = [] roots = [] indent = { tab-width = 2, unit = " " } @@ -74,7 +70,6 @@ name = "c" scope = "source.c" injection-regex = "c" file-types = ["c"] # TODO: ["h"] -shebangs = [] roots = [] comment-token = "//" @@ -86,7 +81,6 @@ 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 = "//" @@ -98,7 +92,6 @@ name = "c-sharp" scope = "source.csharp" injection-regex = "c-?sharp" file-types = ["cs"] -shebangs = [] roots = [] comment-token = "//" @@ -109,7 +102,6 @@ name = "go" scope = "source.go" injection-regex = "go" file-types = ["go"] -shebangs = [] roots = ["Gopkg.toml", "go.mod"] auto-format = true comment-token = "//" @@ -123,7 +115,7 @@ name = "javascript" scope = "source.js" injection-regex = "^(js|javascript)$" file-types = ["js", "mjs"] -shebangs = [] +shebangs = ["node"] roots = [] comment-token = "//" # TODO: highlights-jsx, highlights-params @@ -147,7 +139,6 @@ name = "tsx" scope = "source.tsx" injection-regex = "^(tsx)$" # |typescript file-types = ["tsx"] -shebangs = [] roots = [] # TODO: highlights-jsx, highlights-params @@ -159,7 +150,6 @@ name = "css" scope = "source.css" injection-regex = "css" file-types = ["css"] -shebangs = [] roots = [] indent = { tab-width = 2, unit = " " } @@ -169,7 +159,6 @@ name = "html" scope = "text.html.basic" injection-regex = "html" file-types = ["html"] -shebangs = [] roots = [] indent = { tab-width = 2, unit = " " } @@ -238,7 +227,6 @@ name = "latex" scope = "source.tex" injection-regex = "tex" file-types = ["tex"] -shebangs = [] roots = [] comment-token = "%" @@ -249,7 +237,6 @@ name = "julia" scope = "source.julia" injection-regex = "julia" file-types = ["jl"] -shebangs = [] roots = [] comment-token = "#" language-server = { command = "julia", args = [ @@ -275,7 +262,6 @@ name = "java" scope = "source.java" injection-regex = "java" file-types = ["java"] -shebangs = [] roots = [] indent = { tab-width = 4, unit = " " } @@ -284,7 +270,6 @@ name = "ledger" scope = "source.ledger" injection-regex = "ledger" file-types = ["ldg", "ledger", "journal"] -shebangs = [] roots = [] comment-token = ";" indent = { tab-width = 4, unit = " " } @@ -312,7 +297,7 @@ indent = { tab-width = 2, unit = " "} name = "lua" scope = "source.lua" file-types = ["lua"] -shebangs = [] +shebangs = ["lua"] roots = [] comment-token = "--" indent = { tab-width = 2, unit = " " } @@ -322,7 +307,6 @@ name = "svelte" scope = "source.svelte" injection-regex = "svelte" file-types = ["svelte"] -shebangs = [] roots = [] indent = { tab-width = 2, unit = " " } language-server = { command = "svelteserver", args = ["--stdio"] } @@ -333,7 +317,6 @@ name = "vue" scope = "source.vue" injection-regex = "vue" file-types = ["vue"] -shebangs = [] roots = [] indent = { tab-width = 2, unit = " " } @@ -341,7 +324,6 @@ indent = { tab-width = 2, unit = " " } name = "yaml" scope = "source.yaml" file-types = ["yml", "yaml"] -shebangs = [] roots = [] comment-token = "#" indent = { tab-width = 2, unit = " " } @@ -361,7 +343,6 @@ name = "zig" scope = "source.zig" injection-regex = "zig" file-types = ["zig"] -shebangs = [] roots = ["build.zig"] auto-format = true comment-token = "//" @@ -386,7 +367,6 @@ language-server = { command = "swipl", args = [ name = "tsq" scope = "source.tsq" file-types = ["scm"] -shebangs = [] roots = [] comment-token = ";" indent = { tab-width = 2, unit = " " } @@ -395,7 +375,6 @@ indent = { tab-width = 2, unit = " " } name = "cmake" scope = "source.cmake" file-types = ["cmake", "CMakeLists.txt"] -shebangs = [] roots = [] comment-token = "#" indent = { tab-width = 2, unit = " " } @@ -405,7 +384,6 @@ language-server = { command = "cmake-language-server" } name = "glsl" scope = "source.glsl" file-types = ["glsl", "vert", "tesc", "tese", "geom", "frag", "comp" ] -shebangs = [] roots = [] comment-token = "//" indent = { tab-width = 4, unit = " " } -- cgit v1.2.3-70-g09d2 From e18198aeb2ef46681d404fe6adc8ffbd507f1db3 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Tue, 9 Nov 2021 10:58:23 +0900 Subject: Revert "fix(core): stop merging array toml config values (#1004)" It breaks languages.toml merging This reverts commit 4304b52ff86f829115cb89b20160b4f80dbb3dbd. --- helix-core/src/lib.rs | 55 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 21 deletions(-) (limited to 'helix-core') diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 6168d02c..d1720df0 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -103,7 +103,25 @@ pub fn cache_dir() -> std::path::PathBuf { pub fn merge_toml_values(left: toml::Value, right: toml::Value) -> toml::Value { use toml::Value; + fn get_name(v: &Value) -> Option<&str> { + v.get("name").and_then(Value::as_str) + } + match (left, right) { + (Value::Array(mut left_items), Value::Array(right_items)) => { + left_items.reserve(right_items.len()); + for rvalue in right_items { + let lvalue = get_name(&rvalue) + .and_then(|rname| left_items.iter().position(|v| get_name(v) == Some(rname))) + .map(|lpos| left_items.remove(lpos)); + let mvalue = match lvalue { + Some(lvalue) => merge_toml_values(lvalue, rvalue), + None => rvalue, + }; + left_items.push(mvalue); + } + Value::Array(left_items) + } (Value::Table(mut left_map), Value::Table(right_map)) => { for (rname, rvalue) in right_map { match left_map.remove(&rname) { @@ -131,13 +149,12 @@ mod merge_toml_tests { fn language_tomls() { use toml::Value; - const USER: &str = r#" + const USER: &str = " [[language]] - name = "typescript" - test = "bbb" - indent = { tab-width = 4, unit = " ", test = "aaa" } - language-server = { command = "deno", args = ["lsp"] } - "#; + name = \"nix\" + test = \"bbb\" + indent = { tab-width = 4, unit = \" \", test = \"aaa\" } + "; let base: Value = toml::from_slice(include_bytes!("../../languages.toml")) .expect("Couldn't parse built-in langauges config"); @@ -145,27 +162,23 @@ mod merge_toml_tests { let merged = merge_toml_values(base, user); let languages = merged.get("language").unwrap().as_array().unwrap(); - let ts = languages + let nix = languages .iter() - .find(|v| v.get("name").unwrap().as_str().unwrap() == "typescript") + .find(|v| v.get("name").unwrap().as_str().unwrap() == "nix") .unwrap(); - let ts_indent = ts.get("indent").unwrap(); + let nix_indent = nix.get("indent").unwrap(); // We changed tab-width and unit in indent so check them if they are the new values - assert_eq!(ts_indent.get("tab-width").unwrap().as_integer().unwrap(), 4); - assert_eq!(ts_indent.get("unit").unwrap().as_str().unwrap(), " "); - // We added a new keys, so check them - assert_eq!(ts.get("test").unwrap().as_str().unwrap(), "bbb"); - assert_eq!(ts_indent.get("test").unwrap().as_str().unwrap(), "aaa"); assert_eq!( - ts.get("language-server") - .unwrap() - .get("args") - .unwrap() - .as_array() - .unwrap(), - &vec![Value::String("lsp".into())], + nix_indent.get("tab-width").unwrap().as_integer().unwrap(), + 4 ); + assert_eq!(nix_indent.get("unit").unwrap().as_str().unwrap(), " "); + // We added a new keys, so check them + assert_eq!(nix.get("test").unwrap().as_str().unwrap(), "bbb"); + assert_eq!(nix_indent.get("test").unwrap().as_str().unwrap(), "aaa"); + // We didn't change comment-token so it should be same + assert_eq!(nix.get("comment-token").unwrap().as_str().unwrap(), "#"); } } -- cgit v1.2.3-70-g09d2 From a424ef4e209c0483385acedffd2331534f0ce906 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Mon, 8 Nov 2021 21:07:54 -0500 Subject: Use default `languages.toml` if user's is invalid (#994) --- helix-core/src/syntax.rs | 3 ++- helix-term/src/application.rs | 28 ++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) (limited to 'helix-core') diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index a5dd0c59..142265a8 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -40,13 +40,14 @@ where } #[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Configuration { pub language: Vec, } // largely based on tree-sitter/cli/src/loader.rs #[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct LanguageConfiguration { #[serde(rename = "name")] pub language_id: String, diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 6037148f..f1884199 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -60,14 +60,19 @@ impl Application { std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir())); // load default and user config, and merge both - let def_lang_conf: toml::Value = toml::from_slice(include_bytes!("../../languages.toml")) - .expect("Could not parse built-in languages.toml, something must be very wrong"); - let user_lang_conf: Option = std::fs::read(conf_dir.join("languages.toml")) + let builtin_err_msg = + "Could not parse built-in languages.toml, something must be very wrong"; + let def_lang_conf: toml::Value = + toml::from_slice(include_bytes!("../../languages.toml")).expect(builtin_err_msg); + let def_syn_loader_conf: helix_core::syntax::Configuration = + def_lang_conf.clone().try_into().expect(builtin_err_msg); + let user_lang_conf = std::fs::read(conf_dir.join("languages.toml")) .ok() - .map(|raw| toml::from_slice(&raw).expect("Could not parse user languages.toml")); + .map(|raw| toml::from_slice(&raw)); let lang_conf = match user_lang_conf { - Some(value) => merge_toml_values(def_lang_conf, value), - None => def_lang_conf, + Some(Ok(value)) => Ok(merge_toml_values(def_lang_conf, value)), + Some(err @ Err(_)) => err, + None => Ok(def_lang_conf), }; let theme = if let Some(theme) = &config.theme { @@ -83,8 +88,15 @@ impl Application { }; let syn_loader_conf: helix_core::syntax::Configuration = lang_conf - .try_into() - .expect("Could not parse merged (built-in + user) languages.toml"); + .and_then(|conf| conf.try_into()) + .unwrap_or_else(|err| { + eprintln!("Bad language config: {}", err); + eprintln!("Press to continue with default language config"); + use std::io::Read; + // This waits for an enter press. + let _ = std::io::stdin().read(&mut []); + def_syn_loader_conf + }); let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); let mut editor = Editor::new( -- cgit v1.2.3-70-g09d2 From bf95a9ed043242d95e431412e45e218d40a5695a Mon Sep 17 00:00:00 2001 From: Omnikar Date: Thu, 11 Nov 2021 19:34:08 -0500 Subject: Add `remove_selections` command (#1065) * Add `remove_selections` command * Document `remove_selections` * Update helix-term/src/keymap.rs Co-authored-by: Blaž Hrastnik --- book/src/keymap.md | 1 + helix-core/src/selection.rs | 5 +++-- helix-term/src/commands.rs | 19 +++++++++++++++---- helix-term/src/keymap.rs | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) (limited to 'helix-core') diff --git a/book/src/keymap.md b/book/src/keymap.md index 901c8471..7a9a4378 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -101,6 +101,7 @@ | | Expand selection to parent syntax node TODO: pick a key | `expand_selection` | | `J` | Join lines inside selection | `join_selections` | | `K` | Keep selections matching the regex | `keep_selections` | +| `Alt-K` | Remove selections matching the regex | `remove_selections` | | `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index f3b5d2c8..f7c7dbcb 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -528,14 +528,15 @@ impl<'a> IntoIterator for &'a Selection { // TODO: checkSelection -> check if valid for doc length && sorted -pub fn keep_matches( +pub fn keep_or_remove_matches( text: RopeSlice, selection: &Selection, regex: &crate::regex::Regex, + remove: bool, ) -> Option { let result: SmallVec<_> = selection .iter() - .filter(|range| regex.is_match(&range.fragment(text))) + .filter(|range| regex.is_match(&range.fragment(text)) ^ remove) .copied() .collect(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 738621b0..4352ee66 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -302,6 +302,7 @@ impl Command { format_selections, "Format selection", join_selections, "Join lines inside selection", keep_selections, "Keep selections matching regex", + remove_selections, "Remove selections matching regex", keep_primary_selection, "Keep primary selection", remove_primary_selection, "Remove primary selection", completion, "Invoke completion popup", @@ -4320,12 +4321,12 @@ fn join_selections(cx: &mut Context) { doc.append_changes_to_history(view.id); } -fn keep_selections(cx: &mut Context) { - // keep selections matching regex +fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) { + // keep or remove selections matching regex let reg = cx.register.unwrap_or('/'); let prompt = ui::regex_prompt( cx, - "keep:".into(), + if !remove { "keep:" } else { "remove:" }.into(), Some(reg), |_input: &str| Vec::new(), move |view, doc, regex, event| { @@ -4334,7 +4335,9 @@ fn keep_selections(cx: &mut Context) { } let text = doc.text().slice(..); - if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) { + if let Some(selection) = + selection::keep_or_remove_matches(text, doc.selection(view.id), ®ex, remove) + { doc.set_selection(view.id, selection); } }, @@ -4343,6 +4346,14 @@ fn keep_selections(cx: &mut Context) { cx.push_layer(Box::new(prompt)); } +fn keep_selections(cx: &mut Context) { + keep_or_remove_selections_impl(cx, false) +} + +fn remove_selections(cx: &mut Context) { + keep_or_remove_selections_impl(cx, true) +} + fn keep_primary_selection(cx: &mut Context) { let (view, doc) = current!(cx.editor); // TODO: handle count diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index b2b865e4..f79978fb 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -577,7 +577,7 @@ impl Default for Keymaps { "=" => format_selections, "J" => join_selections, "K" => keep_selections, - // TODO: and another method for inverse + "A-K" => remove_selections, "," => keep_primary_selection, "A-," => remove_primary_selection, -- 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-core') 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 1817b7f581f579232a216f92e0cac569b1a80c11 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 15 Nov 2021 00:12:14 +0900 Subject: minor: Import Range too --- helix-core/src/history.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'helix-core') diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index bf2624e2..9fe7e530 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -1,4 +1,4 @@ -use crate::{Assoc, ChangeSet, Rope, State, Transaction}; +use crate::{Assoc, ChangeSet, Range, Rope, State, Transaction}; use once_cell::sync::Lazy; use regex::Regex; use std::num::NonZeroUsize; @@ -148,9 +148,7 @@ impl History { .transaction .changes_iter() // find a change that matches the primary selection - .find(|(from, to, _fragment)| { - crate::Range::new(*from, *to).overlaps(&primary_selection) - }) + .find(|(from, to, _fragment)| Range::new(*from, *to).overlaps(&primary_selection)) // or use the first change .or_else(|| current_revision.transaction.changes_iter().next()) .unwrap(); -- cgit v1.2.3-70-g09d2 From 6fa76d9fe77d43ebc18cc78a6a1c1957d72cf59b Mon Sep 17 00:00:00 2001 From: ath3 Date: Sun, 14 Nov 2021 16:16:20 +0100 Subject: Add trim_selections command (#1092) --- book/src/keymap.md | 1 + helix-core/src/movement.rs | 2 +- helix-term/src/commands.rs | 37 +++++++++++++++++++++++++++++++++++++ helix-term/src/keymap.rs | 2 +- 4 files changed, 40 insertions(+), 2 deletions(-) (limited to 'helix-core') diff --git a/book/src/keymap.md b/book/src/keymap.md index 9f1714f6..69f5f02c 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -88,6 +88,7 @@ | `s` | Select all regex matches inside selections | `select_regex` | | `S` | Split selection into subselections on regex matches | `split_selection` | | `Alt-s` | Split selection on newlines | `split_selection_on_newline` | +| `_` | Trim whitespace from the selection | `trim_selections` | | `;` | Collapse selection onto a single cursor | `collapse_selection` | | `Alt-;` | Flip selection cursor and anchor | `flip_selections` | | `,` | Keep only the primary selection | `keep_primary_selection` | diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index 9e85bd21..01a8f890 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -168,7 +168,7 @@ pub fn backwards_skip_while(slice: RopeSlice, pos: usize, fun: F) -> Option bool, { - let mut chars_starting_from_next = slice.chars_at(pos + 1); + let mut chars_starting_from_next = slice.chars_at(pos); let mut backwards = iter::from_fn(|| chars_starting_from_next.prev()).enumerate(); backwards.find_map(|(i, c)| { if !fun(c) { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ebacb377..56cc02a2 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -272,6 +272,7 @@ impl Command { // TODO: different description ? goto_line_end_newline, "Goto line end", goto_first_nonwhitespace, "Goto first non-blank in line", + trim_selections, "Trim whitespace from selections", extend_to_line_start, "Extend to line start", extend_to_line_end, "Extend to line end", extend_to_line_end_newline, "Extend to line end", @@ -584,6 +585,42 @@ fn goto_first_nonwhitespace(cx: &mut Context) { doc.set_selection(view.id, selection); } +fn trim_selections(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let ranges: SmallVec<[Range; 1]> = doc + .selection(view.id) + .iter() + .filter_map(|range| { + if range.is_empty() || range.fragment(text).chars().all(|ch| ch.is_whitespace()) { + return None; + } + let mut start = range.from(); + let mut end = range.to(); + start = movement::skip_while(text, start, |x| x.is_whitespace()).unwrap_or(start); + end = movement::backwards_skip_while(text, end, |x| x.is_whitespace()).unwrap_or(end); + if range.anchor < range.head { + Some(Range::new(start, end)) + } else { + Some(Range::new(end, start)) + } + }) + .collect(); + + if !ranges.is_empty() { + let primary = doc.selection(view.id).primary(); + let idx = ranges + .iter() + .position(|range| range.overlaps(&primary)) + .unwrap_or(ranges.len() - 1); + doc.set_selection(view.id, Selection::new(ranges, idx)); + } else { + collapse_selection(cx); + keep_primary_selection(cx); + }; +} + fn goto_window(cx: &mut Context, align: Align) { let (view, doc) = current!(cx.editor); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 3280f0f8..fc9fd590 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -603,7 +603,7 @@ impl Default for Keymaps { // "Q" => replay_macro, // & align selections - // _ trim selections + "_" => trim_selections, "(" => rotate_selections_backward, ")" => rotate_selections_forward, -- cgit v1.2.3-70-g09d2 From b7c3877e947d95ee0fd9b1653dab6f65bb340439 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Sun, 14 Nov 2021 23:16:47 +0800 Subject: Add movement shortcut for history (#1088) alt-u and alt-U--- book/src/keymap.md | 2 ++ helix-core/src/history.rs | 2 +- helix-term/src/commands.rs | 60 +++++++++++++++++++++++++++++++++------------- helix-term/src/keymap.rs | 2 ++ 4 files changed, 48 insertions(+), 18 deletions(-) (limited to 'helix-core') diff --git a/book/src/keymap.md b/book/src/keymap.md index 69f5f02c..88610a77 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -61,6 +61,8 @@ | `.` | Repeat last change | N/A | | `u` | Undo change | `undo` | | `U` | Redo change | `redo` | +| `Alt-u` | Move backward in history | `earlier` | +| `Alt-U` | Move forward in history | `later` | | `y` | Yank selection | `yank` | | `p` | Paste after selection | `paste_after` | | `P` | Paste before selection | `paste_before` | diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index 9fe7e530..4b1c8d3b 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -282,7 +282,7 @@ impl History { } /// Whether to undo by a number of edits or a duration of time. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] pub enum UndoKind { Steps(usize), TimePeriod(std::time::Duration), diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 56cc02a2..115d1789 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,5 +1,7 @@ use helix_core::{ - comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent, + comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, + history::UndoKind, + indent, indent::IndentStyle, line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, @@ -284,6 +286,8 @@ impl Command { delete_word_backward, "Delete previous word", undo, "Undo change", redo, "Redo change", + earlier, "Move backward in history", + later, "Move forward in history", yank, "Yank selection", yank_joined_to_clipboard, "Join and yank selections to clipboard", yank_main_selection_to_clipboard, "Yank main selection to clipboard", @@ -1877,10 +1881,7 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - let uk = args - .join(" ") - .parse::() - .map_err(|s| anyhow!(s))?; + let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); let success = doc.earlier(view.id, uk); @@ -1896,10 +1897,7 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - let uk = args - .join(" ") - .parse::() - .map_err(|s| anyhow!(s))?; + let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); let success = doc.later(view.id, uk); if !success { @@ -3963,20 +3961,48 @@ pub mod insert { // storing it? fn undo(cx: &mut Context) { + let count = cx.count(); let (view, doc) = current!(cx.editor); - let view_id = view.id; - let success = doc.undo(view_id); - if !success { - cx.editor.set_status("Already at oldest change".to_owned()); + for _ in 0..count { + if !doc.undo(view.id) { + cx.editor.set_status("Already at oldest change".to_owned()); + break; + } } } fn redo(cx: &mut Context) { + let count = cx.count(); + let (view, doc) = current!(cx.editor); + for _ in 0..count { + if !doc.redo(view.id) { + cx.editor.set_status("Already at newest change".to_owned()); + break; + } + } +} + +fn earlier(cx: &mut Context) { + let count = cx.count(); + let (view, doc) = current!(cx.editor); + for _ in 0..count { + // rather than doing in batch we do this so get error halfway + if !doc.earlier(view.id, UndoKind::Steps(1)) { + cx.editor.set_status("Already at oldest change".to_owned()); + break; + } + } +} + +fn later(cx: &mut Context) { + let count = cx.count(); let (view, doc) = current!(cx.editor); - let view_id = view.id; - let success = doc.redo(view_id); - if !success { - cx.editor.set_status("Already at newest change".to_owned()); + for _ in 0..count { + // rather than doing in batch we do this so get error halfway + if !doc.later(view.id, UndoKind::Steps(1)) { + cx.editor.set_status("Already at newest change".to_owned()); + break; + } } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index fc9fd590..010714dc 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -582,6 +582,8 @@ impl Default for Keymaps { "u" => undo, "U" => redo, + "A-u" => earlier, + "A-U" => later, "y" => yank, // yank_all -- cgit v1.2.3-70-g09d2 From 6cb35d28a878470ef742b813e1e8d412d09e6b52 Mon Sep 17 00:00:00 2001 From: Jason Hansen Date: Mon, 15 Nov 2021 08:32:58 -0700 Subject: Add command to inc/dec number under cursor (#1027) * Add command to inc/dec number under cursor With the cursor over a number in normal mode, Ctrl + A will increment the number and Ctrl + X will decrement the number. It works with binary, octal, decimal, and hexidecimal numbers. Here are some examples. 0b01110100 0o1734 -24234 0x1F245 If the number isn't over a number it will try to find a number after the cursor on the same line. * Move several functions to helix-core * Change to work based on word under selection * It no longer finds the next number if the cursor isn't already over a number. * It only matches numbers that are part of words with other characters like "foo123bar". * It now works with multiple selections. * Add some unit tests * Fix for clippy * Simplify some things * Keep previous selection after incrementing * Use short word instead of long word This change requires us to manually handle minus sign. * Don't pad decimal numbers if no leading zeros * Handle numbers with `_` separators * Refactor and add tests * Move most of the code into core * Add tests for the incremented output * Use correct range * Formatting * Rename increment functions * Make docs more specific * This is easier to read * This is clearer * Type can be inferred--- book/src/keymap.md | 2 + helix-core/src/lib.rs | 1 + helix-core/src/numbers.rs | 499 +++++++++++++++++++++++++++++++++++++++++++++ helix-term/src/commands.rs | 38 ++++ helix-term/src/keymap.rs | 3 + 5 files changed, 543 insertions(+) create mode 100644 helix-core/src/numbers.rs (limited to 'helix-core') diff --git a/book/src/keymap.md b/book/src/keymap.md index 7a896035..335e393b 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -72,6 +72,8 @@ | `=` | Format selection (**LSP**) | `format_selections` | | `d` | Delete selection | `delete_selection` | | `c` | Change selection (delete and enter insert mode) | `change_selection` | +| `Ctrl-a` | Increment object (number) under cursor | `increment` | +| `Ctrl-x` | Decrement object (number) under cursor | `decrement` | #### Shell diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index d1720df0..de7e95c1 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -10,6 +10,7 @@ pub mod line_ending; pub mod macros; pub mod match_brackets; pub mod movement; +pub mod numbers; pub mod object; pub mod path; mod position; diff --git a/helix-core/src/numbers.rs b/helix-core/src/numbers.rs new file mode 100644 index 00000000..e9f3c898 --- /dev/null +++ b/helix-core/src/numbers.rs @@ -0,0 +1,499 @@ +use std::borrow::Cow; + +use ropey::RopeSlice; + +use crate::{ + textobject::{textobject_word, TextObject}, + Range, Tendril, +}; + +#[derive(Debug, PartialEq, Eq)] +pub struct NumberIncrementor<'a> { + pub range: Range, + pub value: i64, + pub radix: u32, + + text: RopeSlice<'a>, +} + +impl<'a> NumberIncrementor<'a> { + /// Return information about number under rang if there is one. + pub fn from_range(text: RopeSlice, range: Range) -> Option { + // If the cursor is on the minus sign of a number we want to get the word textobject to the + // right of it. + let range = if range.to() < text.len_chars() + && range.to() - range.from() <= 1 + && text.char(range.from()) == '-' + { + Range::new(range.from() + 1, range.to() + 1) + } else { + range + }; + + let range = textobject_word(text, range, TextObject::Inside, 1, false); + + // If there is a minus sign to the left of the word object, we want to include it in the range. + let range = if range.from() > 0 && text.char(range.from() - 1) == '-' { + range.extend(range.from() - 1, range.from()) + } else { + range + }; + + let word: String = text + .slice(range.from()..range.to()) + .chars() + .filter(|&c| c != '_') + .collect(); + let (radix, prefixed) = if word.starts_with("0x") { + (16, true) + } else if word.starts_with("0o") { + (8, true) + } else if word.starts_with("0b") { + (2, true) + } else { + (10, false) + }; + + let number = if prefixed { &word[2..] } else { &word }; + + let value = i128::from_str_radix(number, radix).ok()?; + if (value.is_positive() && value.leading_zeros() < 64) + || (value.is_negative() && value.leading_ones() < 64) + { + return None; + } + + let value = value as i64; + Some(NumberIncrementor { + range, + value, + radix, + text, + }) + } + + /// Add `amount` to the number and return the formatted text. + pub fn incremented_text(&self, amount: i64) -> Tendril { + let old_text: Cow = self.text.slice(self.range.from()..self.range.to()).into(); + let old_length = old_text.len(); + let new_value = self.value.wrapping_add(amount); + + // Get separator indexes from right to left. + let separator_rtl_indexes: Vec = old_text + .chars() + .rev() + .enumerate() + .filter_map(|(i, c)| if c == '_' { Some(i) } else { None }) + .collect(); + + let format_length = if self.radix == 10 { + match (self.value.is_negative(), new_value.is_negative()) { + (true, false) => old_length - 1, + (false, true) => old_length + 1, + _ => old_text.len(), + } + } else { + old_text.len() - 2 + } - separator_rtl_indexes.len(); + + let mut new_text = match self.radix { + 2 => format!("0b{:01$b}", new_value, format_length), + 8 => format!("0o{:01$o}", new_value, format_length), + 10 if old_text.starts_with('0') || old_text.starts_with("-0") => { + format!("{:01$}", new_value, format_length) + } + 10 => format!("{}", new_value), + 16 => { + let (lower_count, upper_count): (usize, usize) = + old_text.chars().skip(2).fold((0, 0), |(lower, upper), c| { + ( + lower + c.is_ascii_lowercase().then(|| 1).unwrap_or(0), + upper + c.is_ascii_uppercase().then(|| 1).unwrap_or(0), + ) + }); + if upper_count > lower_count { + format!("0x{:01$X}", new_value, format_length) + } else { + format!("0x{:01$x}", new_value, format_length) + } + } + _ => unimplemented!("radix not supported: {}", self.radix), + }; + + // Add separators from original number. + for &rtl_index in &separator_rtl_indexes { + if rtl_index < new_text.len() { + let new_index = new_text.len() - rtl_index; + new_text.insert(new_index, '_'); + } + } + + // Add in additional separators if necessary. + if new_text.len() > old_length && !separator_rtl_indexes.is_empty() { + let spacing = match separator_rtl_indexes.as_slice() { + [.., b, a] => a - b - 1, + _ => separator_rtl_indexes[0], + }; + + let prefix_length = if self.radix == 10 { 0 } else { 2 }; + if let Some(mut index) = new_text.find('_') { + while index - prefix_length > spacing { + index -= spacing; + new_text.insert(index, '_'); + } + } + } + + new_text.into() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Rope; + + #[test] + fn test_decimal_at_point() { + let rope = Rope::from_str("Test text 12345 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 15), + value: 12345, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_uppercase_hexadecimal_at_point() { + let rope = Rope::from_str("Test text 0x123ABCDEF more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 21), + value: 0x123ABCDEF, + radix: 16, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_lowercase_hexadecimal_at_point() { + let rope = Rope::from_str("Test text 0xfa3b4e more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 18), + value: 0xfa3b4e, + radix: 16, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_octal_at_point() { + let rope = Rope::from_str("Test text 0o1074312 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 19), + value: 0o1074312, + radix: 8, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_binary_at_point() { + let rope = Rope::from_str("Test text 0b10111010010101 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 26), + value: 0b10111010010101, + radix: 2, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_negative_decimal_at_point() { + let rope = Rope::from_str("Test text -54321 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 16), + value: -54321, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_decimal_with_leading_zeroes_at_point() { + let rope = Rope::from_str("Test text 000045326 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 19), + value: 45326, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_negative_decimal_cursor_on_minus_sign() { + let rope = Rope::from_str("Test text -54321 more text."); + let range = Range::point(10); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 16), + value: -54321, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_number_under_range_start_of_rope() { + let rope = Rope::from_str("100"); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(0, 3), + value: 100, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_number_under_range_end_of_rope() { + let rope = Rope::from_str("100"); + let range = Range::point(2); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(0, 3), + value: 100, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_number_surrounded_by_punctuation() { + let rope = Rope::from_str(",100;"); + let range = Range::point(1); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(1, 4), + value: 100, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_not_a_number_point() { + let rope = Rope::from_str("Test text 45326 more text."); + let range = Range::point(6); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_number_too_large_at_point() { + let rope = Rope::from_str("Test text 0xFFFFFFFFFFFFFFFFF more text."); + let range = Range::point(12); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_number_cursor_one_right_of_number() { + let rope = Rope::from_str("100 "); + let range = Range::point(3); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_number_cursor_one_left_of_number() { + let rope = Rope::from_str(" 100"); + let range = Range::point(0); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_increment_basic_decimal_numbers() { + let tests = [ + ("100", 1, "101"), + ("100", -1, "99"), + ("99", 1, "100"), + ("100", 1000, "1100"), + ("100", -1000, "-900"), + ("-1", 1, "0"), + ("-1", 2, "1"), + ("1", -1, "0"), + ("1", -2, "-1"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } + + #[test] + fn test_increment_basic_hexadedimal_numbers() { + let tests = [ + ("0x0100", 1, "0x0101"), + ("0x0100", -1, "0x00ff"), + ("0x0001", -1, "0x0000"), + ("0x0000", -1, "0xffffffffffffffff"), + ("0xffffffffffffffff", 1, "0x0000000000000000"), + ("0xffffffffffffffff", 2, "0x0000000000000001"), + ("0xffffffffffffffff", -1, "0xfffffffffffffffe"), + ("0xABCDEF1234567890", 1, "0xABCDEF1234567891"), + ("0xabcdef1234567890", 1, "0xabcdef1234567891"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } + + #[test] + fn test_increment_basic_octal_numbers() { + let tests = [ + ("0o0107", 1, "0o0110"), + ("0o0110", -1, "0o0107"), + ("0o0001", -1, "0o0000"), + ("0o7777", 1, "0o10000"), + ("0o1000", -1, "0o0777"), + ("0o0107", 10, "0o0121"), + ("0o0000", -1, "0o1777777777777777777777"), + ("0o1777777777777777777777", 1, "0o0000000000000000000000"), + ("0o1777777777777777777777", 2, "0o0000000000000000000001"), + ("0o1777777777777777777777", -1, "0o1777777777777777777776"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } + + #[test] + fn test_increment_basic_binary_numbers() { + let tests = [ + ("0b00000100", 1, "0b00000101"), + ("0b00000100", -1, "0b00000011"), + ("0b00000100", 2, "0b00000110"), + ("0b00000100", -2, "0b00000010"), + ("0b00000001", -1, "0b00000000"), + ("0b00111111", 10, "0b01001001"), + ("0b11111111", 1, "0b100000000"), + ("0b10000000", -1, "0b01111111"), + ( + "0b0000", + -1, + "0b1111111111111111111111111111111111111111111111111111111111111111", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 1, + "0b0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 2, + "0b0000000000000000000000000000000000000000000000000000000000000001", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + -1, + "0b1111111111111111111111111111111111111111111111111111111111111110", + ), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } + + #[test] + fn test_increment_with_separators() { + let tests = [ + ("999_999", 1, "1_000_000"), + ("1_000_000", -1, "999_999"), + ("-999_999", -1, "-1_000_000"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000", -1, "0xffff_ffff_ffff_ffff"), + ("0x0000_0000_0000", -1, "0xffff_ffff_ffff_ffff"), + ("0b01111111_11111111", 1, "0b10000000_00000000"), + ("0b11111111_11111111", 1, "0b1_00000000_00000000"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } +} diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 74bc52fd..7ef8f56c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -6,6 +6,7 @@ use helix_core::{ line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, movement::{self, Direction}, + numbers::NumberIncrementor, object, pos_at_coords, regex::{self, Regex, RegexBuilder}, register::Register, @@ -354,6 +355,8 @@ impl Command { shell_keep_pipe, "Filter selections with shell predicate", suspend, "Suspend", rename_symbol, "Rename symbol", + increment, "Increment", + decrement, "Decrement", ); } @@ -5459,3 +5462,38 @@ fn rename_symbol(cx: &mut Context) { ); cx.push_layer(Box::new(prompt)); } + +/// Increment object under cursor by count. +fn increment(cx: &mut Context) { + increment_impl(cx, cx.count() as i64); +} + +/// Decrement object under cursor by count. +fn decrement(cx: &mut Context) { + increment_impl(cx, -(cx.count() as i64)); +} + +/// Decrement object under cursor by `amount`. +fn increment_impl(cx: &mut Context, amount: i64) { + let (view, doc) = current!(cx.editor); + let selection = doc.selection(view.id); + let text = doc.text(); + + let changes = selection.ranges().iter().filter_map(|range| { + let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?; + let new_text = incrementor.incremented_text(amount); + Some(( + incrementor.range.from(), + incrementor.range.to(), + Some(new_text), + )) + }); + + if changes.clone().count() > 0 { + let transaction = Transaction::change(doc.text(), changes); + let transaction = transaction.with_selection(selection.clone()); + + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); + } +} diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index fdf43d87..bf3b594e 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -698,6 +698,9 @@ impl Default for Keymaps { "A-!" => shell_append_output, "$" => shell_keep_pipe, "C-z" => suspend, + + "C-a" => increment, + "C-x" => decrement, }); let mut select = normal.clone(); select.merge_nodes(keymap!({ "Select mode" -- cgit v1.2.3-70-g09d2 From 90fd09f2cc4e5dfa7380036b0748adf95ffdf371 Mon Sep 17 00:00:00 2001 From: ath3 Date: Thu, 18 Nov 2021 01:49:56 +0100 Subject: Fix selection remove doc comment (#1122) --- helix-core/src/selection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'helix-core') diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index f7c7dbcb..b4d1dffa 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -360,7 +360,7 @@ impl Selection { self.normalize() } - /// Adds a new range to the selection and makes it the primary range. + /// Removes a range from the selection. pub fn remove(mut self, index: usize) -> Self { assert!( self.ranges.len() > 1, -- cgit v1.2.3-70-g09d2 From 2b7c0866538676de4e5738d82e450163ff733104 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 19 Nov 2021 12:06:19 +0900 Subject: fix: Expand tilde first, then deal with relative paths Otherwise the ~ gets treated as a relative path. Fixes #1107 --- helix-core/src/path.rs | 4 ++-- helix-term/src/commands.rs | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) (limited to 'helix-core') diff --git a/helix-core/src/path.rs b/helix-core/src/path.rs index 6c37cfa1..a6644465 100644 --- a/helix-core/src/path.rs +++ b/helix-core/src/path.rs @@ -40,7 +40,6 @@ pub fn expand_tilde(path: &Path) -> PathBuf { /// needs to improve on. /// Copied from cargo: pub fn get_normalized_path(path: &Path) -> PathBuf { - let path = expand_tilde(path); let mut components = path.components().peekable(); let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { components.next(); @@ -72,10 +71,11 @@ pub fn get_normalized_path(path: &Path) -> PathBuf { /// This function is used instead of `std::fs::canonicalize` because we don't want to verify /// here if the path exists, just normalize it's components. pub fn get_canonicalized_path(path: &Path) -> std::io::Result { + let path = expand_tilde(path); let path = if path.is_relative() { std::env::current_dir().map(|current_dir| current_dir.join(path))? } else { - path.to_path_buf() + path }; Ok(get_normalized_path(path.as_path())) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e5cf7bb2..431265cd 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1767,11 +1767,8 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - use helix_core::path::expand_tilde; let path = args.get(0).context("wrong argument count")?; - let _ = cx - .editor - .open(expand_tilde(Path::new(path)), Action::Replace)?; + let _ = cx.editor.open(path.into(), Action::Replace)?; Ok(()) } -- cgit v1.2.3-70-g09d2