diff options
31 files changed, 844 insertions, 117 deletions
diff --git a/.gitmodules b/.gitmodules index d1fc1517..a8e6481e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -122,3 +122,11 @@ path = helix-syntax/languages/tree-sitter-svelte url = https://github.com/Himujjal/tree-sitter-svelte shallow = true +[submodule "helix-syntax/languages/tree-sitter-vue"] + path = helix-syntax/languages/tree-sitter-vue + url = https://github.com/ikatyang/tree-sitter-vue + shallow = true +[submodule "helix-syntax/languages/tree-sitter-tsq"] + path = helix-syntax/languages/tree-sitter-tsq + url = https://github.com/tree-sitter/tree-sitter-tsq + shallow = true @@ -37,9 +37,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" dependencies = [ "lazy_static", "memchr", @@ -54,9 +54,9 @@ checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e" [[package]] name = "bytes" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cassowary" @@ -66,9 +66,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" [[package]] name = "cfg-if" @@ -369,6 +369,7 @@ dependencies = [ "regex", "ropey", "serde", + "serde_json", "similar", "smallvec", "tendril", @@ -531,18 +532,18 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" +checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd" dependencies = [ "cfg-if", ] [[package]] name = "itoa" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "jsonrpc-core" @@ -565,15 +566,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.99" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" +checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" [[package]] name = "libloading" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0" dependencies = [ "cfg-if", "winapi", @@ -581,9 +582,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] @@ -599,9 +600,9 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.90.0" +version = "0.90.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7404037aab080771c90b0a499836d9d8a10336ecd07badf969567b65c6d51a1" +checksum = "6f3734ab1d7d157fc0c45110e06b587c31cd82bea2ccfd6b563cbff0aaeeb1d3" dependencies = [ "bitflags", "serde", @@ -711,9 +712,9 @@ checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] name = "parking_lot" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", @@ -722,9 +723,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ "cfg-if", "instant", @@ -754,9 +755,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro2" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" dependencies = [ "unicode-xid", ] @@ -999,9 +1000,9 @@ checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" [[package]] name = "syn" -version = "1.0.74" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +checksum = "a4eac2e6c19f5c3abc0c229bea31ff0b9b091c7b14990e8924b92902a303a0c0" dependencies = [ "proc-macro2", "quote", @@ -1021,18 +1022,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -1059,9 +1060,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" dependencies = [ "tinyvec_macros", ] @@ -1094,9 +1095,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +checksum = "154794c8f499c2619acd19e839294703e9e32e7630ef5f46ea80d4ef0fbee5eb" dependencies = [ "proc-macro2", "quote", @@ -83,4 +83,6 @@ a good overview of the internals. # Getting help +Your question might already be answered on the [FAQ](https://github.com/helix-editor/helix/wiki/FAQ). + Discuss the project on the community [Matrix Space](https://matrix.to/#/#helix-community:matrix.org) (make sure to join `#helix-editor:matrix.org` if you're on a client that doesn't support Matrix Spaces yet). diff --git a/book/src/configuration.md b/book/src/configuration.md index 60b12bfd..d47f95d9 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -19,6 +19,8 @@ To override global configuration parameters, create a `config.toml` file located | `line-number` | Line number display (`absolute`, `relative`) | `absolute` | | `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` | | `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` | +| `auto-completion` | Enable automatic pop up of auto-completion. | `true` | +| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` | ## LSP diff --git a/book/theme/css/chrome.css b/book/theme/css/chrome.css index 72b7f188..aba8a417 100644 --- a/book/theme/css/chrome.css +++ b/book/theme/css/chrome.css @@ -196,7 +196,7 @@ a > .hljs { border-radius: 3px; } -:not(pre):not(a) > .hljs { +:not(pre):not(a):not(td):not(p) > .hljs { color: var(--inline-code-color); overflow-x: initial; } diff --git a/book/theme/css/general.css b/book/theme/css/general.css index ddc2387a..9b280d08 100644 --- a/book/theme/css/general.css +++ b/book/theme/css/general.css @@ -162,7 +162,6 @@ table thead td { table thead th { padding: .75rem; text-align: left; - color: var(--table-border-color); font-weight: 500; line-height: 1.5; width: auto; @@ -228,3 +227,7 @@ blockquote *:last-child { margin: 5px 0px; font-weight: bold; } + +.result-no-output { + font-style: italic; +} diff --git a/book/theme/css/variables.css b/book/theme/css/variables.css index db1a11b8..b62c7558 100644 --- a/book/theme/css/variables.css +++ b/book/theme/css/variables.css @@ -13,7 +13,6 @@ .ayu { --bg: hsl(210, 25%, 8%); --fg: #c5c5c5; - --heading-fg: #c5c5c5; --sidebar-bg: #14191f; --sidebar-fg: #c8c9db; @@ -54,7 +53,6 @@ .coal { --bg: hsl(200, 7%, 8%); --fg: #98a3ad; - --heading-fg: #98a3ad; --sidebar-bg: #292c2f; --sidebar-fg: #a1adb8; @@ -95,7 +93,6 @@ .light { --bg: hsl(0, 0%, 100%); --fg: hsl(0, 0%, 0%); - --heading-fg: hsl(0, 0%, 0%); --sidebar-bg: #fafafa; --sidebar-fg: hsl(0, 0%, 0%); @@ -110,7 +107,7 @@ --links: #20609f; - --inline-code-color: #a39e9b; + --inline-code-color: #301900; --theme-popup-bg: #fafafa; --theme-popup-border: #cccccc; @@ -136,7 +133,6 @@ .navy { --bg: hsl(226, 23%, 11%); --fg: #bcbdd0; - --heading-fg: #bcbdd0; --sidebar-bg: #282d3f; --sidebar-fg: #c8c9db; @@ -177,7 +173,6 @@ .rust { --bg: hsl(60, 9%, 87%); --fg: #262625; - --heading-fg: #262625; --sidebar-bg: #3b2e2a; --sidebar-fg: #c8c9db; @@ -192,7 +187,7 @@ --links: #2b79a2; - --inline-code-color: #c5c8c6; + --inline-code-color: #6e6b5e; --theme-popup-bg: #e1e1db; --theme-popup-border: #b38f6b; @@ -218,8 +213,7 @@ @media (prefers-color-scheme: dark) { .light.no-js { --bg: hsl(200, 7%, 8%); - --fg: #ebeafa; - --heading-fg: #ebeafa; + --fg: #98a3ad; --sidebar-bg: #292c2f; --sidebar-fg: #a1adb8; @@ -234,7 +228,7 @@ --links: #2b79a2; - --inline-code-color: #6e6b5e; + --inline-code-color: #c5c8c6; --theme-popup-bg: #141617; --theme-popup-border: #43484d; diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 27adceb2..20ba47e9 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -29,6 +29,7 @@ arc-swap = "1" regex = "1" serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" toml = "0.5" similar = "2.1" diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 755ee679..18af4d08 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -29,10 +29,10 @@ use std::borrow::Cow; /// "(anchor, head)", followed by example text with "[" and "]" /// inserted to represent the anchor and head positions: /// -/// - (0, 3): [Som]e text. -/// - (3, 0): ]Som[e text. -/// - (2, 7): So[me te]xt. -/// - (1, 1): S[]ome text. +/// - (0, 3): `[Som]e text`. +/// - (3, 0): `]Som[e text`. +/// - (2, 7): `So[me te]xt`. +/// - (1, 1): `S[]ome text`. /// /// Ranges are considered to be inclusive on the left and /// exclusive on the right, regardless of anchor-head ordering. diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 996823ce..00c09ea2 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -31,6 +31,15 @@ where .transpose() } +fn deserialize_lsp_config<'de, D>(deserializer: D) -> Result<Option<serde_json::Value>, D::Error> +where + D: serde::Deserializer<'de>, +{ + Option::<toml::Value>::deserialize(deserializer)? + .map(|toml| toml.try_into().map_err(serde::de::Error::custom)) + .transpose() +} + #[derive(Debug, Serialize, Deserialize)] pub struct Configuration { pub language: Vec<LanguageConfiguration>, @@ -46,7 +55,9 @@ pub struct LanguageConfiguration { pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc> pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml> pub comment_token: Option<String>, - pub config: Option<String>, + + #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")] + pub config: Option<serde_json::Value>, #[serde(default)] pub auto_format: bool, diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 35cff754..7fa65928 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -318,15 +318,7 @@ impl Registry { let (client, incoming, initialize_notify) = Client::start( &config.command, &config.args, - serde_json::from_str(language_config.config.as_deref().unwrap_or("")) - .map_err(|e| { - log::error!( - "LSP Config, {}, in `languages.toml` for `{}`", - e, - language_config.scope() - ) - }) - .ok(), + language_config.config.clone(), id, )?; self.incoming.push(UnboundedReceiverStream::new(incoming)); diff --git a/helix-syntax/languages/tree-sitter-tsq b/helix-syntax/languages/tree-sitter-tsq new file mode 160000 +Subproject b665659d3238e6036e22ed0e24935e60efb3941 diff --git a/helix-syntax/languages/tree-sitter-vue b/helix-syntax/languages/tree-sitter-vue new file mode 160000 +Subproject 91fe2754796cd8fba5f229505a23fa08f3546c0 diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index b99fccdf..d8ff2a8a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -102,6 +102,7 @@ impl Application { if !args.files.is_empty() { let first = &args.files[0]; // we know it's not empty if first.is_dir() { + std::env::set_current_dir(&first)?; editor.new_file(Action::VerticalSplit); compositor.push(Box::new(ui::file_picker(first.clone()))); } else { @@ -204,6 +205,11 @@ impl Application { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render(); } + _ = &mut self.editor.idle_timer => { + // idle timeout + self.editor.clear_idle_timer(); + self.handle_idle_timeout(); + } } } } @@ -233,6 +239,38 @@ impl Application { } } + pub fn handle_idle_timeout(&mut self) { + use crate::commands::{completion, Context}; + use helix_view::document::Mode; + + if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion { + return; + } + let editor_view = self + .compositor + .find(std::any::type_name::<ui::EditorView>()) + .expect("expected at least one EditorView"); + let editor_view = editor_view + .as_any_mut() + .downcast_mut::<ui::EditorView>() + .unwrap(); + + if editor_view.completion.is_some() { + return; + } + + let mut cx = Context { + register: None, + editor: &mut self.editor, + jobs: &mut self.jobs, + count: None, + callback: None, + on_next_key_callback: None, + }; + completion(&mut cx); + self.render(); + } + pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) { let mut cx = crate::compositor::Context { editor: &mut self.editor, @@ -417,14 +455,6 @@ impl Application { server_id: usize, ) { use helix_lsp::{Call, MethodCall, Notification}; - let editor_view = self - .compositor - .find(std::any::type_name::<ui::EditorView>()) - .expect("expected at least one EditorView"); - let editor_view = editor_view - .as_any_mut() - .downcast_mut::<ui::EditorView>() - .unwrap(); match call { Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => { @@ -534,7 +564,19 @@ impl Application { Notification::LogMessage(params) => { log::info!("window/logMessage: {:?}", params); } - Notification::ProgressMessage(params) => { + Notification::ProgressMessage(params) + if !self + .compositor + .has_component(std::any::type_name::<ui::Prompt>()) => + { + let editor_view = self + .compositor + .find(std::any::type_name::<ui::EditorView>()) + .expect("expected at least one EditorView"); + let editor_view = editor_view + .as_any_mut() + .downcast_mut::<ui::EditorView>() + .unwrap(); let lsp::ProgressParams { token, value } = params; let lsp::ProgressParamsValue::WorkDone(work) = value; @@ -609,6 +651,9 @@ impl Application { self.editor.set_status(status); } } + Notification::ProgressMessage(_params) => { + // do nothing + } } } Call::MethodCall(helix_lsp::jsonrpc::MethodCall { @@ -643,6 +688,14 @@ impl Application { MethodCall::WorkDoneProgressCreate(params) => { self.lsp_progress.create(server_id, params.token); + let editor_view = self + .compositor + .find(std::any::type_name::<ui::EditorView>()) + .expect("expected at least one EditorView"); + let editor_view = editor_view + .as_any_mut() + .downcast_mut::<ui::EditorView>() + .unwrap(); let spinner = editor_view.spinners_mut().get_or_create(server_id); if spinner.is_stopped() { spinner.start(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6a678de1..f3761d7d 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1310,7 +1310,8 @@ fn global_search(cx: &mut Context) { cx.push_layer(Box::new(prompt)); - let root = find_root(None).unwrap_or_else(|| PathBuf::from("./")); + let current_path = doc_mut!(cx.editor).path().cloned(); + let show_picker = async move { let all_matches: Vec<(usize, PathBuf)> = UnboundedReceiverStream::new(all_matches_rx).collect().await; @@ -1320,14 +1321,19 @@ fn global_search(cx: &mut Context) { editor.set_status("No matches found".to_string()); return; } + let picker = FilePicker::new( all_matches, move |(_line_num, path)| { - path.strip_prefix(&root) - .unwrap_or(path) + let relative_path = helix_core::path::get_relative_path(path) .to_str() .unwrap() - .into() + .to_owned(); + if current_path.as_ref().map(|p| p == path).unwrap_or(false) { + format!("{} (*)", relative_path).into() + } else { + relative_path.into() + } }, move |editor: &mut Editor, (line_num, path), action| { match editor.open(path.into(), action) { @@ -4160,7 +4166,7 @@ fn remove_primary_selection(cx: &mut Context) { doc.set_selection(view.id, selection); } -fn completion(cx: &mut Context) { +pub fn completion(cx: &mut Context) { // trigger on trigger char, or if user calls it // (or on word char typing??) // after it's triggered, if response marked is_incomplete, update on every subsequent keypress @@ -4205,10 +4211,8 @@ fn completion(cx: &mut Context) { }; let offset_encoding = language_server.offset_encoding(); - let cursor = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); + let text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding); @@ -4216,6 +4220,15 @@ fn completion(cx: &mut Context) { let trigger_offset = cursor; + // TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply + // completion filtering. For example logger.te| should filter the initial suggestion list with "te". + + use helix_core::chars; + let mut iter = text.chars_at(cursor); + iter.reverse(); + let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count(); + let start_offset = cursor.saturating_sub(offset); + cx.callback( future, move |editor: &mut Editor, @@ -4238,7 +4251,7 @@ fn completion(cx: &mut Context) { }; if items.is_empty() { - editor.set_error("No completion available".to_string()); + // editor.set_error("No completion available".to_string()); return; } let size = compositor.size(); @@ -4246,7 +4259,14 @@ fn completion(cx: &mut Context) { .find(std::any::type_name::<ui::EditorView>()) .unwrap(); if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() { - ui.set_completion(items, offset_encoding, trigger_offset, size); + ui.set_completion( + editor, + items, + offset_encoding, + start_offset, + trigger_offset, + size, + ); }; }, ); diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 36e54ede..cad1df05 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -171,6 +171,12 @@ impl Compositor { (None, CursorKind::Hidden) } + pub fn has_component(&self, type_name: &str) -> bool { + self.layers + .iter() + .any(|component| component.type_name() == type_name) + } + pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> { self.layers .iter_mut() diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 6c9e3a80..c75b24f1 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -69,14 +69,18 @@ impl menu::Item for CompletionItem { /// Wraps a Menu. pub struct Completion { popup: Popup<Menu<CompletionItem>>, + start_offset: usize, + #[allow(dead_code)] trigger_offset: usize, // TODO: maintain a completioncontext with trigger kind & trigger char } impl Completion { pub fn new( + editor: &Editor, items: Vec<CompletionItem>, offset_encoding: helix_lsp::OffsetEncoding, + start_offset: usize, trigger_offset: usize, ) -> Self { // let items: Vec<CompletionItem> = Vec::new(); @@ -175,16 +179,22 @@ impl Completion { }; }); let popup = Popup::new(menu); - Self { + let mut completion = Self { popup, + start_offset, trigger_offset, - } + }; + + // need to recompute immediately in case start_offset != trigger_offset + completion.recompute_filter(editor); + + completion } - pub fn update(&mut self, cx: &mut commands::Context) { + pub fn recompute_filter(&mut self, editor: &Editor) { // recompute menu based on matches let menu = self.popup.contents_mut(); - let (view, doc) = current!(cx.editor); + let (view, doc) = current_ref!(editor); // cx.hooks() // cx.add_hook(enum type, ||) @@ -200,14 +210,22 @@ impl Completion { .selection(view.id) .primary() .cursor(doc.text().slice(..)); - if self.trigger_offset <= cursor { - let fragment = doc.text().slice(self.trigger_offset..cursor); + if self.start_offset <= cursor { + let fragment = doc.text().slice(self.start_offset..cursor); let text = Cow::from(fragment); // TODO: logic is same as ui/picker menu.score(&text); + } else { + // we backspaced before the start offset, clear the menu + // this will cause the editor to remove the completion popup + menu.clear(); } } + pub fn update(&mut self, cx: &mut commands::Context) { + self.recompute_filter(cx.editor) + } + pub fn is_empty(&self) -> bool { self.popup.contents().is_empty() } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 128fe948..037f04b8 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -37,7 +37,7 @@ pub struct EditorView { keymaps: Keymaps, on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>, last_insert: (commands::Command, Vec<KeyEvent>), - completion: Option<Completion>, + pub(crate) completion: Option<Completion>, spinners: ProgressSpinners, autoinfo: Option<Info>, } @@ -984,12 +984,21 @@ impl EditorView { pub fn set_completion( &mut self, + editor: &Editor, items: Vec<helix_lsp::lsp::CompletionItem>, offset_encoding: helix_lsp::OffsetEncoding, + start_offset: usize, trigger_offset: usize, size: Rect, ) { - let mut completion = Completion::new(items, offset_encoding, trigger_offset); + let mut completion = + Completion::new(editor, items, offset_encoding, start_offset, trigger_offset); + + if completion.is_empty() { + // skip if we got no completion results + return; + } + // TODO : propagate required size on resize to completion too completion.required_size((size.width, size.height)); self.completion = Some(completion); @@ -1211,6 +1220,7 @@ impl Component for EditorView { EventResult::Consumed(None) } Event::Key(key) => { + cxt.editor.reset_idle_timer(); let mut key = KeyEvent::from(key); canonicalize_key(&mut key); // clear status @@ -1245,6 +1255,7 @@ impl Component for EditorView { if callback.is_some() { // assume close_fn self.completion = None; + cxt.editor.clear_idle_timer(); // don't retrigger } } } @@ -1258,6 +1269,7 @@ impl Component for EditorView { completion.update(&mut cxt); if completion.is_empty() { self.completion = None; + cxt.editor.clear_idle_timer(); // don't retrigger } } } diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index dab0c34f..055593fd 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -90,6 +90,14 @@ impl<T: Item> Menu<T> { self.recalculate = true; } + pub fn clear(&mut self) { + self.matches.clear(); + + // reset cursor position + self.cursor = None; + self.scroll = 0; + } + pub fn move_up(&mut self) { let len = self.matches.len(); let pos = self.cursor.map_or(0, |i| (i + len.saturating_sub(1)) % len) % len; diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index ee1ec177..341235ee 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -271,12 +271,18 @@ impl<T> Picker<T> { } pub fn move_up(&mut self) { + if self.matches.is_empty() { + return; + } let len = self.matches.len(); let pos = ((self.cursor + len.saturating_sub(1)) % len) % len; self.cursor = pos; } pub fn move_down(&mut self) { + if self.matches.is_empty() { + return; + } let len = self.matches.len(); let pos = (self.cursor + 1) % len; self.cursor = pos; diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 72140ea8..60864e9e 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -13,10 +13,12 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use std::{ collections::HashMap, path::{Path, PathBuf}, + pin::Pin, sync::Arc, - time::Duration, }; +use tokio::time::{sleep, Duration, Instant, Sleep}; + use slotmap::SlotMap; use anyhow::Error; @@ -29,6 +31,14 @@ use helix_dap as dap; use serde::Deserialize; +fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error> +where + D: serde::Deserializer<'de>, +{ + let millis = u64::deserialize(deserializer)?; + Ok(Duration::from_millis(millis)) +} + #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case", default)] pub struct Config { @@ -42,12 +52,17 @@ pub struct Config { pub shell: Vec<String>, /// Line number mode. pub line_number: LineNumber, - /// Middle click paste support. Defaults to true + /// Middle click paste support. Defaults to true. pub middle_click_paste: bool, /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true. pub smart_case: bool, /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true. pub auto_pairs: bool, + /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true. + pub auto_completion: bool, + /// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms. + #[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")] + pub idle_timeout: Duration, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] @@ -75,6 +90,8 @@ impl Default for Config { middle_click_paste: true, smart_case: true, auto_pairs: true, + auto_completion: true, + idle_timeout: Duration::from_millis(400), } } } @@ -105,6 +122,8 @@ pub struct Editor { pub status_msg: Option<(String, Severity)>, pub config: Config, + + pub idle_timer: Pin<Box<Sleep>>, } #[derive(Debug, Copy, Clone)] @@ -146,10 +165,24 @@ impl Editor { registers: Registers::default(), clipboard_provider: get_clipboard_provider(), status_msg: None, + idle_timer: Box::pin(sleep(config.idle_timeout)), config, } } + pub fn clear_idle_timer(&mut self) { + // equivalent to internal Instant::far_future() (30 years) + self.idle_timer + .as_mut() + .reset(Instant::now() + Duration::from_secs(86400 * 365 * 30)); + } + + pub fn reset_idle_timer(&mut self) { + self.idle_timer + .as_mut() + .reset(Instant::now() + self.config.idle_timeout); + } + pub fn clear_status(&mut self) { self.status_msg = None; } diff --git a/helix-view/src/macros.rs b/helix-view/src/macros.rs index c9a04270..0bebd02f 100644 --- a/helix-view/src/macros.rs +++ b/helix-view/src/macros.rs @@ -44,3 +44,19 @@ macro_rules! view { $( $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) + }}; +} diff --git a/languages.toml b/languages.toml index a5640c21..981da557 100644 --- a/languages.toml +++ b/languages.toml @@ -6,19 +6,11 @@ file-types = ["rs"] roots = [] auto-format = true comment-token = "//" -config = """ -{ - "cargo": { - "loadOutDirsFromCheck": true - }, - "procMacro": { - "enable": false - } -} -""" - language-server = { command = "rust-analyzer" } indent = { tab-width = 4, unit = " " } +[language.config] +cargo = { loadOutDirsFromCheck = true } +procMacro = { enable = false } [language.debugger] name = "lldb-vscode" @@ -134,6 +126,16 @@ completion = [ "pid" ] args = { console = "internalConsole", pid = "{0}" } [[language]] +name = "c-sharp" +scope = "source.csharp" +injection-regex = "c-?sharp" +file-types = ["cs"] +roots = [] +comment-token = "//" + +indent = { tab-width = 4, unit = "\t" } + +[[language]] name = "go" scope = "source.go" injection-regex = "go" @@ -248,7 +250,7 @@ file-types = ["py"] roots = [] comment-token = "#" -language-server = { command = "pyls" } +language-server = { command = "pylsp" } # TODO: pyls needs utf-8 offsets indent = { tab-width = 4, unit = " " } @@ -380,6 +382,15 @@ roots = [] indent = { tab-width = 2, unit = " " } language-server = { command = "svelteserver", args = ["--stdio"] } + +[[language]] +name = "vue" +scope = "source.vue" +injection-regex = "vue" +file-types = ["vue"] +roots = [] +indent = { tab-width = 2, unit = " " } + [[language]] name = "yaml" scope = "source.yaml" @@ -409,3 +420,23 @@ comment-token = "//" language-server = { command = "zls" } indent = { tab-width = 4, unit = " " } + +[[language]] +name = "prolog" +scope = "source.prolog" +roots = [] +file-types = ["pl", "prolog"] +comment-token = "%" + +language-server = { command = "swipl", args = [ + "-g", "use_module(library(lsp_server))", + "-g", "lsp_server:main", + "-t", "halt", "--", "stdio"] } + +[[language]] +name = "tsq" +scope = "source.tsq" +file-types = ["scm"] +roots = [] +comment-token = ";" +indent = { tab-width = 2, unit = " " } diff --git a/runtime/queries/c-sharp/highlights.scm b/runtime/queries/c-sharp/highlights.scm new file mode 100644 index 00000000..b76f4e60 --- /dev/null +++ b/runtime/queries/c-sharp/highlights.scm @@ -0,0 +1,238 @@ +;; Methods +(method_declaration (identifier) @type (identifier) @function) + +;; Types +(interface_declaration name: (identifier) @type) +(class_declaration name: (identifier) @type) +(enum_declaration name: (identifier) @type) +(struct_declaration (identifier) @type) +(record_declaration (identifier) @type) +(namespace_declaration name: (identifier) @type) + +(constructor_declaration name: (identifier) @type) + +[ + (implicit_type) + (nullable_type) + (pointer_type) + (function_pointer_type) + (predefined_type) +] @type.builtin + +;; Enum +(enum_member_declaration (identifier) @variable.property) + +;; Literals +[ + (real_literal) + (integer_literal) +] @number + +[ + (character_literal) + (string_literal) + (verbatim_string_literal) + (interpolated_string_text) + (interpolated_verbatim_string_text) + "\"" + "$\"" + "@$\"" + "$@\"" + ] @string + +[ + (boolean_literal) + (null_literal) + (void_keyword) +] @constant.builtin + +;; Comments +(comment) @comment + +;; Tokens +[ + ";" + "." + "," +] @punctuation.delimiter + +[ + "--" + "-" + "-=" + "&" + "&&" + "+" + "++" + "+=" + "<" + "<<" + "=" + "==" + "!" + "!=" + "=>" + ">" + ">>" + "|" + "||" + "?" + "??" + "^" + "~" + "*" + "/" + "%" + ":" +] @operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +;; Keywords +(modifier) @keyword +(this_expression) @keyword +(escape_sequence) @keyword + +[ + "as" + "base" + "break" + "case" + "catch" + "checked" + "class" + "continue" + "default" + "delegate" + "do" + "else" + "enum" + "event" + "explicit" + "finally" + "for" + "foreach" + "goto" + "if" + "implicit" + "interface" + "is" + "lock" + "namespace" + "operator" + "params" + "return" + "sizeof" + "stackalloc" + "struct" + "switch" + "throw" + "try" + "typeof" + "unchecked" + "using" + "while" + "new" + "await" + "in" + "yield" + "get" + "set" + "when" + "out" + "ref" + "from" + "where" + "select" + "record" + "init" + "with" + "let" +] @keyword + + +;; Linq +(from_clause (identifier) @variable) +(group_clause) +(order_by_clause) +(select_clause (identifier) @variable) +(query_continuation (identifier) @variable) @keyword + +;; Record +(with_expression + (with_initializer_expression + (simple_assignment_expression + (identifier) @variable))) + +;; Exprs +(binary_expression (identifier) @variable (identifier) @variable) +(binary_expression (identifier)* @variable) +(conditional_expression (identifier) @variable) +(prefix_unary_expression (identifier) @variable) +(postfix_unary_expression (identifier)* @variable) +(assignment_expression (identifier) @variable) +(cast_expression (identifier) @type (identifier) @variable) + +;; Class +(base_list (identifier) @type) +(property_declaration (generic_name)) +(property_declaration + type: (nullable_type) @type + name: (identifier) @variable) +(property_declaration + type: (predefined_type) @type + name: (identifier) @variable) +(property_declaration + type: (identifier) @type + name: (identifier) @variable) + +;; Lambda +(lambda_expression) @variable + +;; Attribute +(attribute) @type + +;; Parameter +(parameter + type: (identifier) @type + name: (identifier) @variable.parameter) +(parameter (identifier) @variable.parameter) +(parameter_modifier) @keyword + +;; Typeof +(type_of_expression (identifier) @type) + +;; Variable +(variable_declaration (identifier) @type) +(variable_declarator (identifier) @variable) + +;; Return +(return_statement (identifier) @variable) +(yield_statement (identifier) @variable) + +;; Type +(generic_name (identifier) @type) +(type_parameter (identifier) @variable.parameter) +(type_argument_list (identifier) @type) + +;; Type constraints +(type_parameter_constraints_clause (identifier) @variable.parameter) +(type_constraint (identifier) @type) + +;; Exception +(catch_declaration (identifier) @type (identifier) @variable) +(catch_declaration (identifier) @type) + +;; Switch +(switch_statement (identifier) @variable) +(switch_expression (identifier) @variable) + +;; Lock statement +(lock_statement (identifier) @variable) diff --git a/runtime/queries/java/highlights.scm b/runtime/queries/java/highlights.scm index 3f8ae0d5..e7d793df 100644 --- a/runtime/queries/java/highlights.scm +++ b/runtime/queries/java/highlights.scm @@ -47,7 +47,7 @@ ; Variables ((identifier) @constant - (#match? @constant "^_*[A-Z][A-Z\d_]+")) + (#match? @constant "^_*[A-Z][A-Z\\d_]+$")) (identifier) @variable diff --git a/runtime/queries/php/highlights.scm b/runtime/queries/php/highlights.scm index 83850403..02904555 100644 --- a/runtime/queries/php/highlights.scm +++ b/runtime/queries/php/highlights.scm @@ -42,7 +42,7 @@ (relative_scope) @variable.builtin ((name) @constant - (#match? @constant "^_?[A-Z][A-Z\d_]+$")) + (#match? @constant "^_?[A-Z][A-Z\\d_]+$")) ((name) @constructor (#match? @constructor "^[A-Z]")) diff --git a/runtime/queries/tsq/highlights.scm b/runtime/queries/tsq/highlights.scm new file mode 100644 index 00000000..9ba5699a --- /dev/null +++ b/runtime/queries/tsq/highlights.scm @@ -0,0 +1,46 @@ +; mark the string passed #match? as a regex +(((predicate_name) @function + (capture) + (string) @string.regexp) + (#eq? @function "#match?")) + +; highlight inheritance comments +((query . (comment) @keyword.directive) + (#match? @keyword.directive "^;\ +inherits *:")) + +[ + "(" + ")" + "[" + "]" +] @punctuation.bracket + +":" @punctuation.delimiter + +[ + (one_or_more) + (zero_or_one) + (zero_or_more) +] @operator + +[ + (wildcard_node) + (anchor) +] @constant.builtin + +[ + (anonymous_leaf) + (string) +] @string + +(comment) @comment + +(field_name) @property + +(capture) @label + +(predicate_name) @function + +(escape_sequence) @escape + +(node_name) @variable diff --git a/runtime/queries/vue/highlights.scm b/runtime/queries/vue/highlights.scm new file mode 100644 index 00000000..f90ae429 --- /dev/null +++ b/runtime/queries/vue/highlights.scm @@ -0,0 +1,21 @@ +(tag_name) @tag +(end_tag) @tag + +(directive_name) @keyword +(directive_argument) @constant + +(attribute + (attribute_name) @attribute + (quoted_attribute_value + (attribute_value) @string) +) + +(comment) @comment + +[ + "<" + ">" + "</" + "{{" + "}}" +] @punctuation.bracket
\ No newline at end of file diff --git a/runtime/queries/vue/injections.scm b/runtime/queries/vue/injections.scm new file mode 100644 index 00000000..8ee34ffb --- /dev/null +++ b/runtime/queries/vue/injections.scm @@ -0,0 +1,17 @@ +(directive_attribute + (directive_name) @keyword + (quoted_attribute_value + (attribute_value) @injection.content) + (#set! injection.language "javascript")) + +((interpolation + (raw_text) @injection.content) + (#set! injection.language "javascript")) + +((script_element + (raw_text) @injection.content) + (#set! injection.language "javascript")) + +((style_element + (raw_text) @injection.content) + (#set! injection.language "css")) diff --git a/runtime/themes/nord.toml b/runtime/themes/nord.toml index c6a0e172..ee7c8865 100644 --- a/runtime/themes/nord.toml +++ b/runtime/themes/nord.toml @@ -1,7 +1,7 @@ # Author : RayGervais<raygervais@hotmail.ca> # "ui.linenr.selected" = { fg = "#d8dee9" } -# "ui.text.focus" = { fg = "#e5ded6", modifiers= ["bold"] } +"ui.text.focus" = { fg = "#88c0d0", modifiers= ["bold"] } # "ui.menu.selected" = { fg = "#e5ded6", bg = "#313f4e" } # "info" = "#b48ead" @@ -24,8 +24,8 @@ "ui.cursor.match" = { bg = "434c5e" } # nord3 - comments -"comment" = "#4c566a" -"ui.linenr" = { fg = "#4c566a" } +"comment" = "#616E88" +"ui.linenr" = { fg = "#616E88" } # Snow Storm # nord4 - cursor, variables, constants, attributes, fields diff --git a/runtime/tutor.txt b/runtime/tutor.txt index 07b88884..b6f600d0 100644 --- a/runtime/tutor.txt +++ b/runtime/tutor.txt @@ -20,7 +20,6 @@ _________________________________________________________________ the first lesson. - ================================================================= = BASIC CURSOR MOVEMENT = ================================================================= @@ -43,7 +42,6 @@ _________________________________________________________________ - ================================================================= = EXITING HELIX = ================================================================= @@ -66,7 +64,6 @@ _________________________________________________________________ - ================================================================= = DELETION = ================================================================= @@ -89,7 +86,6 @@ _________________________________________________________________ - ================================================================= = INSERT MODE = ================================================================= @@ -112,7 +108,6 @@ _________________________________________________________________ Note: The status bar will display your current mode. Notice that when you press i, 'NOR' changes to 'INS'. - ================================================================= = MORE ON INSERT MODE = ================================================================= @@ -123,7 +118,7 @@ _________________________________________________________________ Common examples of insertion commands include: i - Insert before the selection. - a - Insert after the selection. (a means "append") + a - Insert after the selection. (a means 'append') I - Insert at the start of the line. A - Insert at the end of the line. @@ -135,7 +130,6 @@ _________________________________________________________________ --> This sentence is miss This sentence is missing some text. - ================================================================= = SAVING A FILE = ================================================================= @@ -158,7 +152,6 @@ _________________________________________________________________ - ================================================================= = RECAP = ================================================================= @@ -181,7 +174,6 @@ _________________________________________________________________ - ================================================================= = MOTIONS AND SELECTIONS = ================================================================= @@ -204,7 +196,6 @@ _________________________________________________________________ - ================================================================= = MORE ON MOTIONS = ================================================================= @@ -227,6 +218,203 @@ _________________________________________________________________ +================================================================= += THE CHANGE COMMAND = +================================================================= + + Press c to change the current selection. + + The change command deletes the current selection and enters + Insert mode, so it is a very common shorthand for di. + + 1. Move the cursor to the line below marked -->. + 2. Move to the start of an incorrect word and press w to + select it. + 3. Press c to delete the word and enter Insert mode. + 4. Type the correct word. + 5. Repeat until the line matches the line below it. + + --> This paper has heavy words behind it. + This sentence has incorrect words in it. + + + + +================================================================= += COUNTS WITH MOTIONS = +================================================================= + + Type a number before a motion to repeat it that many times. + + 1. Move the cursor to the line below marked -->. + + 2. Type 2w to move 2 words forward. + + 3. Type 3e to move to the end of the third word forward. + + 4. Type 2b to move 2 words backwards + + 5. Try the above with different numbers. + + --> This is just a line with words you can move around in. + + + + + +================================================================= += SELECTING LINES = +================================================================= + + Press x to select a whole line. Press again to select the next. + + 1. Move the cursor to the second line below marked -->. + 2. Press x to select the line, and d to delete it. + 3. Move to the fourth line. + 4. Press x twice or type 2x to select 2 lines, and d to delete. + + --> 1) Roses are red, + --> 2) Mud is fun, + --> 3) Violets are blue, + --> 4) I have a car, + --> 5) Clocks tell time, + --> 6) Sugar is sweet, + --> 7) And so are you. + + + + +================================================================= += UNDOING = +================================================================= + + Type u to undo. Type U to redo. + + 1. Move the cursor to the line below marked -->. + 2. Move to the first error, and press d to delete it. + 3. Type u to undo your deletion. + 4. Fix all the errors on the line. + 5. Type u several times to undo your fixes. + 6. Type U (<SHIFT> + u) several times to redo your fixes. + + --> Fiix the errors on thhis line and reeplace them witth undo. + + + + + + + + +================================================================= += RECAP = +================================================================= + + * Type w to select forward until the next word. + * Type e to select to the end of the current word. + * Type b to select backward to the start of the current word. + * Use uppercase counterparts, W,E,B, to traverse WORDS. + + * Typing d deletes the entire selection, so you can delete a + word forward by typing wd. + + * Type c to delete the selection and enter Insert mode. + + * Type a number before a motion to repeat it that many times. + + * Type x to select the entire current line. Type x again to + select the next line. + + * Type u to undo. Type U to redo. + + +================================================================= += MULTIPLE CURSORS = +================================================================= + + Type C to duplicate the cursor to the next line. + + 1. Move the cursor to the first line below marked -->. + 2. Type C to duplicate the cursor to the next line. Keys you + press will now affect both cursors. + 3. Use Insert mode to correct the lines. The two cursors will + fix both lines simultaneously. + 4. Type , to remove the second cursor. + + --> Fix th two nes at same ime. + --> Fix th two nes at same ime. + + Fix these two lines at the same time. + + + + + +================================================================= += THE SELECT COMMAND = +================================================================= + + Type s to select matches in the selection. + + 1. Move the cursor to the line below marked -->. + 2. Press x to select the line. + 3. Press s. A prompt will appear. + 4. Type 'apples' and press <ENTER>. Both occurrences of + 'apples' in the line will be selected. + 5. You can now press c and change 'apples' to something else, + like 'oranges'. + 6. Type , to remove the second cursor. + + --> I like to eat apples since my favorite fruit is apples. + + + + + + +================================================================= += SELECTING VIA REGEX = +================================================================= + + The select command selects regular expressions, not just exact + matches, allowing you to target more complex patterns. + + 1. Move the cursor to the line below marked -->. + 2. Select the line with x and then press s. + 3. Enter ' +' to select any amount of consecutive spaces >1. + 4. Press c and change the matches to single spaces. + + --> This sentence has some extra spaces. + + Note: If you want to perform find-and-replace, the select + command is the way to do it. Select the text you want + to replace in — type % to select the whole file — and + then perform the steps explained above. + + + + +================================================================= += COLLAPSING SELECTIONS = +================================================================= + + Type ; to collapse selections to single cursors. + + Sometimes, you want to deselect without having to move the + cursor(s). This can be done using the ; key. + + 1. Move the cursor to the line below marked -->. + + 2. Use the motions you have learned to move around the line, + and try using ; to deselect the text after it is selected + by the motions. + + --> This is an error-free line with words to move around in. + + + + + ================================================================= This tutorial is still a work-in-progress. |