aboutsummaryrefslogtreecommitdiff
path: root/helix-term
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term')
-rw-r--r--helix-term/Cargo.toml17
-rw-r--r--helix-term/build.rs15
-rw-r--r--helix-term/src/application.rs139
-rw-r--r--helix-term/src/args.rs45
-rw-r--r--helix-term/src/commands.rs1358
-rw-r--r--helix-term/src/commands/dap.rs8
-rw-r--r--helix-term/src/compositor.rs36
-rw-r--r--helix-term/src/config.rs51
-rw-r--r--helix-term/src/job.rs18
-rw-r--r--helix-term/src/keymap.rs68
-rw-r--r--helix-term/src/lib.rs11
-rw-r--r--helix-term/src/main.rs2
-rw-r--r--helix-term/src/ui/completion.rs55
-rw-r--r--helix-term/src/ui/editor.rs292
-rw-r--r--helix-term/src/ui/markdown.rs310
-rw-r--r--helix-term/src/ui/menu.rs41
-rw-r--r--helix-term/src/ui/mod.rs34
-rw-r--r--helix-term/src/ui/picker.rs41
-rw-r--r--helix-term/src/ui/popup.rs49
-rw-r--r--helix-term/src/ui/prompt.rs10
-rw-r--r--helix-term/src/ui/spinner.rs9
21 files changed, 1770 insertions, 839 deletions
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index 43268291..e62496f2 100644
--- a/helix-term/Cargo.toml
+++ b/helix-term/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "helix-term"
-version = "0.5.0"
+version = "0.6.0"
description = "A post-modern text editor."
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
@@ -9,6 +9,7 @@ categories = ["editor", "command-line-utilities"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"]
+default-run = "hx"
[package.metadata.nix]
build = true
@@ -21,18 +22,18 @@ name = "hx"
path = "src/main.rs"
[dependencies]
-helix-core = { version = "0.5", path = "../helix-core" }
-helix-view = { version = "0.5", path = "../helix-view" }
-helix-lsp = { version = "0.5", path = "../helix-lsp" }
-helix-dap = { version = "0.5", path = "../helix-dap" }
+helix-core = { version = "0.6", path = "../helix-core" }
+helix-view = { version = "0.6", path = "../helix-view" }
+helix-lsp = { version = "0.6", path = "../helix-lsp" }
+helix-dap = { version = "0.6", path = "../helix-dap" }
anyhow = "1"
-once_cell = "1.8"
+once_cell = "1.9"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
num_cpus = "1"
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
-crossterm = { version = "0.22", features = ["event-stream"] }
+crossterm = { version = "0.23", features = ["event-stream"] }
signal-hook = "0.3"
tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@@ -46,7 +47,7 @@ log = "0.4"
fuzzy-matcher = "0.3"
ignore = "0.4"
# markdown doc rendering
-pulldown-cmark = { version = "0.8", default-features = false }
+pulldown-cmark = { version = "0.9", default-features = false }
# file type detection
content_inspector = "0.2.4"
diff --git a/helix-term/build.rs b/helix-term/build.rs
index 61ffa6f4..21dd5612 100644
--- a/helix-term/build.rs
+++ b/helix-term/build.rs
@@ -1,12 +1,17 @@
+use std::borrow::Cow;
use std::process::Command;
fn main() {
let git_hash = Command::new("git")
- .args(&["describe", "--dirty"])
+ .args(&["rev-parse", "HEAD"])
.output()
- .map(|x| String::from_utf8(x.stdout).ok())
.ok()
- .flatten()
- .unwrap_or_else(|| String::from(env!("CARGO_PKG_VERSION")));
- println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", git_hash);
+ .and_then(|x| String::from_utf8(x.stdout).ok());
+
+ let version: Cow<_> = match git_hash {
+ Some(git_hash) => format!("{} ({})", env!("CARGO_PKG_VERSION"), &git_hash[..8]).into(),
+ None => env!("CARGO_PKG_VERSION").into(),
+ };
+
+ println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
}
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 55e4bb03..52a5321f 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -1,10 +1,16 @@
-use helix_core::{merge_toml_values, syntax};
+use helix_core::{merge_toml_values, pos_at_coords, syntax, Selection};
use helix_dap::{self as dap, Payload, Request};
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
use helix_view::{editor::Breakpoint, theme, Editor};
+use serde_json::json;
use crate::{
- args::Args, commands::fetch_stack_trace, compositor::Compositor, config::Config, job::Jobs, ui,
+ args::Args,
+ commands::{align_view, apply_workspace_edit, fetch_stack_trace, Align},
+ compositor::Compositor,
+ config::Config,
+ job::Jobs,
+ ui,
};
use log::{error, warn};
@@ -78,17 +84,27 @@ impl Application {
None => Ok(def_lang_conf),
};
- let theme = if let Some(theme) = &config.theme {
- match theme_loader.load(theme) {
- Ok(theme) => theme,
- Err(e) => {
- log::warn!("failed to load theme `{}` - {}", theme, e);
+ let true_color = config.editor.true_color || crate::true_color();
+ let theme = config
+ .theme
+ .as_ref()
+ .and_then(|theme| {
+ theme_loader
+ .load(theme)
+ .map_err(|e| {
+ log::warn!("failed to load theme `{}` - {}", theme, e);
+ e
+ })
+ .ok()
+ .filter(|theme| (true_color || theme.is_16_color()))
+ })
+ .unwrap_or_else(|| {
+ if true_color {
theme_loader.default()
+ } else {
+ theme_loader.base16_default()
}
- }
- } else {
- theme_loader.default()
- };
+ });
let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
.and_then(|conf| conf.try_into())
@@ -118,7 +134,7 @@ impl Application {
// Unset path to prevent accidentally saving to the original tutor file.
doc_mut!(editor).set_path(None)?;
} else if !args.files.is_empty() {
- let first = &args.files[0]; // we know it's not empty
+ let first = &args.files[0].0; // we know it's not empty
if first.is_dir() {
std::env::set_current_dir(&first)?;
editor.new_file(Action::VerticalSplit);
@@ -126,16 +142,25 @@ impl Application {
} else {
let nr_of_files = args.files.len();
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
- for file in args.files {
+ for (file, pos) in args.files {
if file.is_dir() {
return Err(anyhow::anyhow!(
"expected a path to file, found a directory. (to open a directory pass it as first argument)"
));
} else {
- editor.open(file.to_path_buf(), Action::Load)?;
+ let doc_id = editor.open(file, Action::Load)?;
+ // with Action::Load all documents have the same view
+ let view_id = editor.tree.focus;
+ let doc = editor.document_mut(doc_id).unwrap();
+ let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
+ doc.set_selection(view_id, pos);
}
}
editor.set_status(format!("Loaded {} files.", nr_of_files));
+ // align the view to center after all files are loaded,
+ // does not affect views without pos since it is at the top
+ let (view, doc) = current!(editor);
+ align_view(doc, view, Align::Center);
}
} else if stdin().is_tty() {
editor.new_file(Action::VerticalSplit);
@@ -197,7 +222,6 @@ impl Application {
loop {
if self.editor.should_close() {
- self.jobs.finish();
break;
}
@@ -328,7 +352,7 @@ impl Application {
None => return,
};
match payload {
- Payload::Event(ev) => match ev {
+ Payload::Event(ev) => match *ev {
Event::Stopped(events::Stopped {
thread_id,
description,
@@ -529,12 +553,8 @@ impl Application {
// trigger textDocument/didOpen for docs that are already open
for doc in docs {
- // TODO: extract and share with editor.open
- let language_id = doc
- .language()
- .and_then(|s| s.split('.').last()) // source.rust
- .map(ToOwned::to_owned)
- .unwrap_or_default();
+ let language_id =
+ doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
tokio::spawn(language_server.text_document_did_open(
doc.url().unwrap(),
@@ -549,6 +569,7 @@ impl Application {
let doc = self.editor.document_by_path_mut(&path);
if let Some(doc) = doc {
+ let lang_conf = doc.language_config();
let text = doc.text();
let diagnostics = params
@@ -586,19 +607,31 @@ impl Application {
return None;
};
+ let severity =
+ diagnostic.severity.map(|severity| match severity {
+ DiagnosticSeverity::ERROR => Error,
+ DiagnosticSeverity::WARNING => Warning,
+ DiagnosticSeverity::INFORMATION => Info,
+ DiagnosticSeverity::HINT => Hint,
+ severity => unreachable!(
+ "unrecognized diagnostic severity: {:?}",
+ severity
+ ),
+ });
+
+ if let Some(lang_conf) = lang_conf {
+ if let Some(severity) = severity {
+ if severity < lang_conf.diagnostic_severity {
+ return None;
+ }
+ }
+ };
+
Some(Diagnostic {
range: Range { start, end },
line: diagnostic.range.start.line as usize,
message: diagnostic.message,
- severity: diagnostic.severity.map(
- |severity| match severity {
- DiagnosticSeverity::ERROR => Error,
- DiagnosticSeverity::WARNING => Warning,
- DiagnosticSeverity::INFORMATION => Info,
- DiagnosticSeverity::HINT => Hint,
- severity => unimplemented!("{:?}", severity),
- },
- ),
+ severity,
// code
// source
})
@@ -705,14 +738,6 @@ impl Application {
Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
method, params, id, ..
}) => {
- let language_server = match self.editor.language_servers.get_by_id(server_id) {
- Some(language_server) => language_server,
- None => {
- warn!("can't find language server with id `{}`", server_id);
- return;
- }
- };
-
let call = match MethodCall::parse(&method, params) {
Some(call) => call,
None => {
@@ -742,8 +767,42 @@ impl Application {
if spinner.is_stopped() {
spinner.start();
}
+ let language_server =
+ match self.editor.language_servers.get_by_id(server_id) {
+ Some(language_server) => language_server,
+ None => {
+ warn!("can't find language server with id `{}`", server_id);
+ return;
+ }
+ };
+
tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null)));
}
+ MethodCall::ApplyWorkspaceEdit(params) => {
+ apply_workspace_edit(
+ &mut self.editor,
+ helix_lsp::OffsetEncoding::Utf8,
+ &params.edit,
+ );
+
+ let language_server =
+ match self.editor.language_servers.get_by_id(server_id) {
+ Some(language_server) => language_server,
+ None => {
+ warn!("can't find language server with id `{}`", server_id);
+ return;
+ }
+ };
+
+ tokio::spawn(language_server.reply(
+ id,
+ Ok(json!(lsp::ApplyWorkspaceEditResponse {
+ applied: true,
+ failure_reason: None,
+ failed_change: None,
+ })),
+ ));
+ }
}
}
e => unreachable!("{:?}", e),
@@ -789,6 +848,8 @@ impl Application {
self.event_loop().await;
+ self.jobs.finish().await;
+
if self.editor.close_language_servers(None).await.is_err() {
log::error!("Timed out waiting for language servers to shutdown");
};
diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs
index 40113db9..247d5b32 100644
--- a/helix-term/src/args.rs
+++ b/helix-term/src/args.rs
@@ -1,5 +1,6 @@
use anyhow::{Error, Result};
-use std::path::PathBuf;
+use helix_core::Position;
+use std::path::{Path, PathBuf};
#[derive(Default)]
pub struct Args {
@@ -7,7 +8,7 @@ pub struct Args {
pub display_version: bool,
pub load_tutor: bool,
pub verbosity: u64,
- pub files: Vec<PathBuf>,
+ pub files: Vec<(PathBuf, Position)>,
}
impl Args {
@@ -41,15 +42,49 @@ impl Args {
}
}
}
- arg => args.files.push(PathBuf::from(arg)),
+ arg => args.files.push(parse_file(arg)),
}
}
// push the remaining args, if any to the files
- for filename in iter {
- args.files.push(PathBuf::from(filename));
+ for arg in iter {
+ args.files.push(parse_file(arg));
}
Ok(args)
}
}
+
+/// Parse arg into [`PathBuf`] and position.
+pub(crate) fn parse_file(s: &str) -> (PathBuf, Position) {
+ let def = || (PathBuf::from(s), Position::default());
+ if Path::new(s).exists() {
+ return def();
+ }
+ split_path_row_col(s)
+ .or_else(|| split_path_row(s))
+ .unwrap_or_else(def)
+}
+
+/// Split file.rs:10:2 into [`PathBuf`], row and col.
+///
+/// Does not validate if file.rs is a file or directory.
+fn split_path_row_col(s: &str) -> Option<(PathBuf, Position)> {
+ let mut s = s.rsplitn(3, ':');
+ let col: usize = s.next()?.parse().ok()?;
+ let row: usize = s.next()?.parse().ok()?;
+ let path = s.next()?.into();
+ let pos = Position::new(row.saturating_sub(1), col.saturating_sub(1));
+ Some((path, pos))
+}
+
+/// Split file.rs:10 into [`PathBuf`] and row.
+///
+/// Does not validate if file.rs is a file or directory.
+fn split_path_row(s: &str) -> Option<(PathBuf, Position)> {
+ let (row, path) = s.rsplit_once(':')?;
+ let row: usize = row.parse().ok()?;
+ let path = path.into();
+ let pos = Position::new(row.saturating_sub(1), 0);
+ Some((path, pos))
+}
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 1871c67e..677943e8 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -5,15 +5,17 @@ pub use dap::*;
use helix_core::{
comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
history::UndoKind,
+ increment::date_time::DateTimeIncrementor,
+ increment::{number::NumberIncrementor, Increment},
indent,
indent::IndentStyle,
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},
- search, selection, surround, textobject,
+ search, selection, shellwords, surround, textobject,
+ tree_sitter::Node,
unicode::width::UnicodeWidthChar,
LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
Transaction,
@@ -22,13 +24,15 @@ use helix_view::{
clipboard::ClipboardType,
document::{Mode, SCRATCH_BUFFER_NAME},
editor::{Action, Motion},
+ info::Info,
input::KeyEvent,
keyboard::KeyCode,
view::View,
Document, DocumentId, Editor, ViewId,
};
-use anyhow::{anyhow, bail, Context as _};
+use anyhow::{anyhow, bail, ensure, Context as _};
+use fuzzy_matcher::FuzzyMatcher;
use helix_lsp::{
block_on, lsp,
util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
@@ -38,14 +42,15 @@ use insert::*;
use movement::Movement;
use crate::{
+ args,
compositor::{self, Component, Compositor},
- ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent},
+ ui::{self, FilePicker, Popup, Prompt, PromptEvent},
};
use crate::job::{self, Job, Jobs};
use futures_util::{FutureExt, StreamExt};
-use std::num::NonZeroUsize;
use std::{collections::HashMap, fmt, future::Future};
+use std::{collections::HashSet, num::NonZeroUsize};
use std::{
borrow::Cow,
@@ -73,7 +78,7 @@ pub struct Context<'a> {
impl<'a> Context<'a> {
/// Push a new component onto the compositor.
pub fn push_layer(&mut self, component: Box<dyn Component>) {
- self.callback = Some(Box::new(|compositor: &mut Compositor| {
+ self.callback = Some(Box::new(|compositor: &mut Compositor, _| {
compositor.push(component)
}));
}
@@ -138,47 +143,76 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) {
view.offset.row = line.saturating_sub(relative);
}
-/// A command is composed of a static name, and a function that takes the current state plus a count,
-/// and does a side-effect on the state (usually by creating and applying a transaction).
-#[derive(Copy, Clone)]
-pub struct Command {
- name: &'static str,
- fun: fn(cx: &mut Context),
- doc: &'static str,
-}
-
-macro_rules! commands {
+/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like
+/// :format. It causes a side-effect on the state (usually by creating and applying a transaction).
+/// Both of these types of commands can be mapped with keybindings in the config.toml.
+#[derive(Clone)]
+pub enum MappableCommand {
+ Typable {
+ name: String,
+ args: Vec<String>,
+ doc: String,
+ },
+ Static {
+ name: &'static str,
+ fun: fn(cx: &mut Context),
+ doc: &'static str,
+ },
+}
+
+macro_rules! static_commands {
( $($name:ident, $doc:literal,)* ) => {
$(
#[allow(non_upper_case_globals)]
- pub const $name: Self = Self {
+ pub const $name: Self = Self::Static {
name: stringify!($name),
fun: $name,
doc: $doc
};
)*
- pub const COMMAND_LIST: &'static [Self] = &[
+ pub const STATIC_COMMAND_LIST: &'static [Self] = &[
$( Self::$name, )*
];
}
}
-impl Command {
+impl MappableCommand {
pub fn execute(&self, cx: &mut Context) {
- (self.fun)(cx);
+ match &self {
+ Self::Typable { name, args, doc: _ } => {
+ let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
+ if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) {
+ let mut cx = compositor::Context {
+ editor: cx.editor,
+ jobs: cx.jobs,
+ scroll: None,
+ };
+ if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) {
+ cx.editor.set_error(format!("{}", e));
+ }
+ }
+ }
+ Self::Static { fun, .. } => (fun)(cx),
+ }
}
- pub fn name(&self) -> &'static str {
- self.name
+ pub fn name(&self) -> &str {
+ match &self {
+ Self::Typable { name, .. } => name,
+ Self::Static { name, .. } => name,
+ }
}
- pub fn doc(&self) -> &'static str {
- self.doc
+ pub fn doc(&self) -> &str {
+ match &self {
+ Self::Typable { doc, .. } => doc,
+ Self::Static { doc, .. } => doc,
+ }
}
#[rustfmt::skip]
- commands!(
+ static_commands!(
no_op, "Do nothing",
move_char_left, "Move left",
move_char_right, "Move right",
@@ -240,6 +274,7 @@ impl Command {
change_selection_noyank, "Change selection (delete and enter insert mode, without yanking)",
collapse_selection, "Collapse selection onto a single cursor",
flip_selections, "Flip selection cursor and anchor",
+ ensure_selections_forward, "Ensure the selection is in forward direction",
insert_mode, "Insert before selection",
append_mode, "Insert after selection (append)",
command_mode, "Enter command mode",
@@ -261,16 +296,17 @@ impl Command {
add_newline_below, "Add newline below",
goto_type_definition, "Goto type definition",
goto_implementation, "Goto implementation",
- goto_file_start, "Goto file start/line",
+ goto_file_start, "Goto line number <n> else file start",
goto_file_end, "Goto file end",
- goto_file, "Goto files in the selection",
- goto_file_hsplit, "Goto files in the selection in horizontal splits",
- goto_file_vsplit, "Goto files in the selection in vertical splits",
+ goto_file, "Goto files in selection",
+ goto_file_hsplit, "Goto files in selection (hsplit)",
+ goto_file_vsplit, "Goto files in selection (vsplit)",
goto_reference, "Goto references",
goto_window_top, "Goto window top",
- goto_window_middle, "Goto window middle",
+ goto_window_center, "Goto window center",
goto_window_bottom, "Goto window bottom",
goto_last_accessed_file, "Goto last accessed file",
+ goto_last_modified_file, "Goto last modified file",
goto_last_modification, "Goto last modification",
goto_line, "Goto line",
goto_last_line, "Goto last line",
@@ -333,8 +369,12 @@ impl Command {
rotate_selection_contents_forward, "Rotate selection contents forward",
rotate_selection_contents_backward, "Rotate selections contents backward",
expand_selection, "Expand selection to parent syntax node",
+ shrink_selection, "Shrink selection to previously expanded syntax node",
+ select_next_sibling, "Select the next sibling in the syntax tree",
+ select_prev_sibling, "Select the previous sibling in the syntax tree",
jump_forward, "Jump forward on jumplist",
jump_backward, "Jump backward on jumplist",
+ save_selection, "Save the current selection to the jumplist",
jump_view_right, "Jump to the split to the right",
jump_view_left, "Jump to the split to the left",
jump_view_up, "Jump to the split above",
@@ -382,36 +422,56 @@ impl Command {
rename_symbol, "Rename symbol",
increment, "Increment",
decrement, "Decrement",
+ record_macro, "Record macro",
+ replay_macro, "Replay macro",
);
}
-impl fmt::Debug for Command {
+impl fmt::Debug for MappableCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let Command { name, .. } = self;
- f.debug_tuple("Command").field(name).finish()
+ f.debug_tuple("MappableCommand")
+ .field(&self.name())
+ .finish()
}
}
-impl fmt::Display for Command {
+impl fmt::Display for MappableCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let Command { name, .. } = self;
- f.write_str(name)
+ f.write_str(self.name())
}
}
-impl std::str::FromStr for Command {
+impl std::str::FromStr for MappableCommand {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- Command::COMMAND_LIST
- .iter()
- .copied()
- .find(|cmd| cmd.name == s)
- .ok_or_else(|| anyhow!("No command named '{}'", s))
+ if let Some(suffix) = s.strip_prefix(':') {
+ let mut typable_command = suffix.split(' ').into_iter().map(|arg| arg.trim());
+ let name = typable_command
+ .next()
+ .ok_or_else(|| anyhow!("Expected typable command name"))?;
+ let args = typable_command
+ .map(|s| s.to_owned())
+ .collect::<Vec<String>>();
+ cmd::TYPABLE_COMMAND_MAP
+ .get(name)
+ .map(|cmd| MappableCommand::Typable {
+ name: cmd.name.to_owned(),
+ doc: format!(":{} {:?}", cmd.name, args),
+ args,
+ })
+ .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s))
+ } else {
+ MappableCommand::STATIC_COMMAND_LIST
+ .iter()
+ .find(|cmd| cmd.name() == s)
+ .cloned()
+ .ok_or_else(|| anyhow!("No command named '{}'", s))
+ }
}
}
-impl<'de> Deserialize<'de> for Command {
+impl<'de> Deserialize<'de> for MappableCommand {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
@@ -421,9 +481,27 @@ impl<'de> Deserialize<'de> for Command {
}
}
-impl PartialEq for Command {
+impl PartialEq for MappableCommand {
fn eq(&self, other: &Self) -> bool {
- self.name() == other.name()
+ match (self, other) {
+ (
+ MappableCommand::Typable {
+ name: first_name, ..
+ },
+ MappableCommand::Typable {
+ name: second_name, ..
+ },
+ ) => first_name == second_name,
+ (
+ MappableCommand::Static {
+ name: first_name, ..
+ },
+ MappableCommand::Static {
+ name: second_name, ..
+ },
+ ) => first_name == second_name,
+ _ => false,
+ }
}
}
@@ -622,8 +700,15 @@ fn kill_to_line_end(cx: &mut Context) {
let selection = doc.selection(view.id).clone().transform(|range| {
let line = range.cursor_line(text);
- let pos = line_end_char_index(&text, line);
- range.put_cursor(text, pos, true)
+ let line_end_pos = line_end_char_index(&text, line);
+ let pos = range.cursor(text);
+
+ let mut new_range = range.put_cursor(text, line_end_pos, true);
+ // don't want to remove the line separator itself if the cursor doesn't reach the end of line.
+ if pos != line_end_pos {
+ new_range.head = line_end_pos;
+ }
+ new_range
});
delete_selection_insert_mode(doc, view, &selection);
}
@@ -736,7 +821,6 @@ fn align_selections(cx: &mut Context) {
});
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> String {
@@ -770,8 +854,8 @@ fn goto_window(cx: &mut Context, align: Align) {
Align::Center => (view.offset.row + ((last_line - view.offset.row) / 2)),
Align::Bottom => last_line.saturating_sub(scrolloff + count),
}
- .min(last_line.saturating_sub(scrolloff))
- .max(view.offset.row + scrolloff);
+ .max(view.offset.row + scrolloff)
+ .min(last_line.saturating_sub(scrolloff));
let pos = doc.text().line_to_char(line);
@@ -782,7 +866,7 @@ fn goto_window_top(cx: &mut Context) {
goto_window(cx, Align::Top)
}
-fn goto_window_middle(cx: &mut Context) {
+fn goto_window_center(cx: &mut Context) {
goto_window(cx, Align::Center)
}
@@ -1139,7 +1223,6 @@ fn replace(cx: &mut Context) {
});
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
})
}
@@ -1157,7 +1240,6 @@ where
});
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
fn switch_case(cx: &mut Context) {
@@ -1222,16 +1304,23 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
.max(view.offset.row + scrolloff)
.min(last_line.saturating_sub(scrolloff));
- let head = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end
+ // If cursor needs moving, replace primary selection
+ if line != cursor.row {
+ let head = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end
- let anchor = if doc.mode == Mode::Select {
- range.anchor
- } else {
- head
- };
+ let anchor = if doc.mode == Mode::Select {
+ range.anchor
+ } else {
+ head
+ };
- // TODO: only manipulate main selection
- doc.set_selection(view.id, Selection::single(anchor, head));
+ // replace primary selection with an empty selection at cursor pos
+ let prim_sel = Range::new(anchor, head);
+ let mut sel = doc.selection(view.id).clone();
+ let idx = sel.primary_index();
+ sel = sel.replace(idx, prim_sel);
+ doc.set_selection(view.id, sel);
+ }
}
fn page_up(cx: &mut Context) {
@@ -1389,6 +1478,7 @@ fn split_selection_on_newline(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
+#[allow(clippy::too_many_arguments)]
fn search_impl(
doc: &mut Document,
view: &mut View,
@@ -1397,6 +1487,7 @@ fn search_impl(
movement: Movement,
direction: Direction,
scrolloff: usize,
+ wrap_around: bool,
) {
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
@@ -1422,16 +1513,22 @@ fn search_impl(
// use find_at to find the next match after the cursor, loop around the end
// Careful, `Regex` uses `bytes` as offsets, not character indices!
- let mat = match direction {
- Direction::Forward => regex
- .find_at(contents, start)
- .or_else(|| regex.find(contents)),
- Direction::Backward => regex.find_iter(&contents[..start]).last().or_else(|| {
- offset = start;
- regex.find_iter(&contents[start..]).last()
- }),
+ let mut mat = match direction {
+ Direction::Forward => regex.find_at(contents, start),
+ Direction::Backward => regex.find_iter(&contents[..start]).last(),
};
- // TODO: message on wraparound
+
+ if wrap_around && mat.is_none() {
+ mat = match direction {
+ Direction::Forward => regex.find(contents),
+ Direction::Backward => {
+ offset = start;
+ regex.find_iter(&contents[start..]).last()
+ }
+ }
+ // TODO: message on wraparound
+ }
+
if let Some(mat) = mat {
let start = text.byte_to_char(mat.start() + offset);
let end = text.byte_to_char(mat.end() + offset);
@@ -1483,8 +1580,9 @@ fn rsearch(cx: &mut Context) {
fn searcher(cx: &mut Context, direction: Direction) {
let reg = cx.register.unwrap_or('/');
let scrolloff = cx.editor.config.scrolloff;
+ let wrap_around = cx.editor.config.search.wrap_around;
- let (_, doc) = current!(cx.editor);
+ let doc = doc!(cx.editor);
// TODO: could probably share with select_on_matches?
@@ -1516,6 +1614,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
Movement::Move,
direction,
scrolloff,
+ wrap_around,
);
},
);
@@ -1530,16 +1629,27 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
if let Some(query) = registers.read('/') {
let query = query.last().unwrap();
let contents = doc.text().slice(..).to_string();
- let case_insensitive = if cx.editor.config.smart_case {
+ let search_config = &cx.editor.config.search;
+ let case_insensitive = if search_config.smart_case {
!query.chars().any(char::is_uppercase)
} else {
false
};
+ let wrap_around = search_config.wrap_around;
if let Ok(regex) = RegexBuilder::new(query)
.case_insensitive(case_insensitive)
.build()
{
- search_impl(doc, view, &contents, &regex, movement, direction, scrolloff);
+ search_impl(
+ doc,
+ view,
+ &contents,
+ &regex,
+ movement,
+ direction,
+ scrolloff,
+ wrap_around,
+ );
} else {
// get around warning `mutable_borrow_reservation_conflict`
// which will be a hard error in the future
@@ -1571,14 +1681,14 @@ fn search_selection(cx: &mut Context) {
let query = doc.selection(view.id).primary().fragment(contents);
let regex = regex::escape(&query);
cx.editor.registers.get_mut('/').push(regex);
- let msg = format!("register '{}' set to '{}'", '\\', query);
+ let msg = format!("register '{}' set to '{}'", '/', query);
cx.editor.set_status(msg);
}
fn global_search(cx: &mut Context) {
let (all_matches_sx, all_matches_rx) =
tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
- let smart_case = cx.editor.config.smart_case;
+ let smart_case = cx.editor.config.search.smart_case;
let file_picker_config = cx.editor.config.file_picker.clone();
let completions = search_completions(cx, None);
@@ -1789,7 +1899,6 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
match op {
Operation::Delete => {
- doc.append_changes_to_history(view.id);
// exit select mode, if currently in select mode
exit_select_mode(cx);
}
@@ -1845,7 +1954,21 @@ fn flip_selections(cx: &mut Context) {
let selection = doc
.selection(view.id)
.clone()
- .transform(|range| Range::new(range.head, range.anchor));
+ .transform(|range| range.flip());
+ doc.set_selection(view.id, selection);
+}
+
+fn ensure_selections_forward(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+
+ let selection = doc
+ .selection(view.id)
+ .clone()
+ .transform(|r| match r.direction() {
+ Direction::Forward => r,
+ Direction::Backward => r.flip(),
+ });
+
doc.set_selection(view.id, selection);
}
@@ -1879,7 +2002,7 @@ fn append_mode(cx: &mut Context) {
if !last_range.is_empty() && last_range.head == end {
let transaction = Transaction::change(
doc.text(),
- std::array::IntoIter::new([(end, end, Some(doc.line_ending.as_str().into()))]),
+ [(end, end, Some(doc.line_ending.as_str().into()))].into_iter(),
);
doc.apply(&transaction, view.id);
}
@@ -1893,7 +2016,7 @@ fn append_mode(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
-mod cmd {
+pub mod cmd {
use super::*;
use helix_view::editor::Action;
@@ -1905,13 +2028,13 @@ mod cmd {
pub aliases: &'static [&'static str],
pub doc: &'static str,
// params, flags, helper, completer
- pub fun: fn(&mut compositor::Context, &[&str], PromptEvent) -> anyhow::Result<()>,
+ pub fun: fn(&mut compositor::Context, &[Cow<str>], PromptEvent) -> anyhow::Result<()>,
pub completer: Option<Completer>,
}
fn quit(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
// last view and we have unsaved changes
@@ -1926,7 +2049,7 @@ mod cmd {
fn force_quit(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
cx.editor.close(view!(cx.editor).id);
@@ -1936,17 +2059,25 @@ mod cmd {
fn open(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let path = args.get(0).context("wrong argument count")?;
- let _ = cx.editor.open(path.into(), Action::Replace)?;
+ ensure!(!args.is_empty(), "wrong argument count");
+ for arg in args {
+ let (path, pos) = args::parse_file(arg);
+ let _ = cx.editor.open(path, Action::Replace)?;
+ let (view, doc) = current!(cx.editor);
+ let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
+ doc.set_selection(view.id, pos);
+ // does not affect opening a buffer without pos
+ align_view(doc, view, Align::Center);
+ }
Ok(())
}
fn buffer_close(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let view = view!(cx.editor);
@@ -1957,7 +2088,7 @@ mod cmd {
fn force_buffer_close(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let view = view!(cx.editor);
@@ -1966,15 +2097,12 @@ mod cmd {
Ok(())
}
- fn write_impl<P: AsRef<Path>>(
- cx: &mut compositor::Context,
- path: Option<P>,
- ) -> anyhow::Result<()> {
+ fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> {
let jobs = &mut cx.jobs;
- let (_, doc) = current!(cx.editor);
+ let doc = doc_mut!(cx.editor);
if let Some(ref path) = path {
- doc.set_path(Some(path.as_ref()))
+ doc.set_path(Some(path.as_ref().as_ref()))
.context("invalid filepath")?;
}
if doc.path().is_none() {
@@ -2003,7 +2131,7 @@ mod cmd {
fn write(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
write_impl(cx, args.first())
@@ -2011,7 +2139,7 @@ mod cmd {
fn new_file(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
cx.editor.new_file(Action::Replace);
@@ -2021,11 +2149,10 @@ mod cmd {
fn format(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let (_, doc) = current!(cx.editor);
-
+ let doc = doc!(cx.editor);
if let Some(format) = doc.format() {
let callback =
make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
@@ -2036,7 +2163,7 @@ mod cmd {
}
fn set_indent_style(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
use IndentStyle::*;
@@ -2056,7 +2183,7 @@ mod cmd {
// Attempt to parse argument as an indent style.
let style = match args.get(0) {
Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
- Some(&"0") => Some(Tabs),
+ Some(Cow::Borrowed("0")) => Some(Tabs),
Some(arg) => arg
.parse::<u8>()
.ok()
@@ -2075,7 +2202,7 @@ mod cmd {
/// Sets or reports the current document's line ending setting.
fn set_line_ending(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
use LineEnding::*;
@@ -2119,7 +2246,7 @@ mod cmd {
fn earlier(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@@ -2135,7 +2262,7 @@ mod cmd {
fn later(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@@ -2150,7 +2277,7 @@ mod cmd {
fn write_quit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_impl(cx, args.first())?;
@@ -2159,7 +2286,7 @@ mod cmd {
fn force_write_quit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_impl(cx, args.first())?;
@@ -2190,13 +2317,13 @@ mod cmd {
fn write_all_impl(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
quit: bool,
force: bool,
) -> anyhow::Result<()> {
let mut errors = String::new();
-
+ let jobs = &mut cx.jobs;
// save all documents
for doc in &mut cx.editor.documents.values_mut() {
if doc.path().is_none() {
@@ -2204,9 +2331,23 @@ mod cmd {
continue;
}
- // TODO: handle error.
- let handle = doc.save();
- cx.jobs.add(Job::new(handle).wait_before_exiting());
+ if !doc.is_modified() {
+ continue;
+ }
+
+ let fmt = doc.auto_format().map(|fmt| {
+ let shared = fmt.shared();
+ let callback = make_format_callback(
+ doc.id(),
+ doc.version(),
+ Modified::SetUnmodified,
+ shared.clone(),
+ );
+ jobs.callback(callback);
+ shared
+ });
+ let future = doc.format_and_save(fmt);
+ jobs.add(Job::new(future).wait_before_exiting());
}
if quit {
@@ -2226,7 +2367,7 @@ mod cmd {
fn write_all(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_all_impl(cx, args, event, false, false)
@@ -2234,7 +2375,7 @@ mod cmd {
fn write_all_quit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_all_impl(cx, args, event, true, false)
@@ -2242,18 +2383,13 @@ mod cmd {
fn force_write_all_quit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_all_impl(cx, args, event, true, true)
}
- fn quit_all_impl(
- editor: &mut Editor,
- _args: &[&str],
- _event: PromptEvent,
- force: bool,
- ) -> anyhow::Result<()> {
+ fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> {
if !force {
buffers_remaining_impl(editor)?;
}
@@ -2269,23 +2405,23 @@ mod cmd {
fn quit_all(
cx: &mut compositor::Context,
- args: &[&str],
- event: PromptEvent,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
) -> anyhow::Result<()> {
- quit_all_impl(&mut cx.editor, args, event, false)
+ quit_all_impl(cx.editor, false)
}
fn force_quit_all(
cx: &mut compositor::Context,
- args: &[&str],
- event: PromptEvent,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
) -> anyhow::Result<()> {
- quit_all_impl(&mut cx.editor, args, event, true)
+ quit_all_impl(cx.editor, true)
}
fn cquit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let exit_code = args
@@ -2294,95 +2430,110 @@ mod cmd {
.unwrap_or(1);
cx.editor.exit_code = exit_code;
- let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
- for view_id in views {
- cx.editor.close(view_id);
- }
+ quit_all_impl(cx.editor, false)
+ }
- Ok(())
+ fn force_cquit(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let exit_code = args
+ .first()
+ .and_then(|code| code.parse::<i32>().ok())
+ .unwrap_or(1);
+ cx.editor.exit_code = exit_code;
+
+ quit_all_impl(cx.editor, true)
}
fn theme(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let theme = args.first().context("theme not provided")?;
- cx.editor.set_theme_from_name(theme)
+ let theme = args.first().context("Theme not provided")?;
+ let theme = cx
+ .editor
+ .theme_loader
+ .load(theme)
+ .with_context(|| format!("Failed setting theme {}", theme))?;
+ let true_color = cx.editor.config.true_color || crate::true_color();
+ if !(true_color || theme.is_16_color()) {
+ bail!("Unsupported theme: theme requires true color support");
+ }
+ cx.editor.set_theme(theme);
+ Ok(())
}
fn yank_main_selection_to_clipboard(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard)
+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
}
fn yank_joined_to_clipboard(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let (_, doc) = current!(cx.editor);
- let separator = args
- .first()
- .copied()
- .unwrap_or_else(|| doc.line_ending.as_str());
- yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Clipboard)
+ let doc = doc!(cx.editor);
+ let default_sep = Cow::Borrowed(doc.line_ending.as_str());
+ let separator = args.first().unwrap_or(&default_sep);
+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
}
fn yank_main_selection_to_primary_clipboard(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection)
+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
}
fn yank_joined_to_primary_clipboard(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let (_, doc) = current!(cx.editor);
- let separator = args
- .first()
- .copied()
- .unwrap_or_else(|| doc.line_ending.as_str());
- yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Selection)
+ let doc = doc!(cx.editor);
+ let default_sep = Cow::Borrowed(doc.line_ending.as_str());
+ let separator = args.first().unwrap_or(&default_sep);
+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
}
fn paste_clipboard_after(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
}
fn paste_clipboard_before(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
}
fn paste_primary_clipboard_after(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
}
fn paste_primary_clipboard_before(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
}
fn replace_selections_with_clipboard_impl(
@@ -2409,7 +2560,7 @@ mod cmd {
fn replace_selections_with_clipboard(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
@@ -2417,7 +2568,7 @@ mod cmd {
fn replace_selections_with_primary_clipboard(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
@@ -2425,7 +2576,7 @@ mod cmd {
fn show_clipboard_provider(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
cx.editor
@@ -2435,12 +2586,13 @@ mod cmd {
fn change_current_directory(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let dir = helix_core::path::expand_tilde(
args.first()
.context("target directory not provided")?
+ .as_ref()
.as_ref(),
);
@@ -2458,7 +2610,7 @@ mod cmd {
fn show_current_directory(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
@@ -2470,10 +2622,10 @@ mod cmd {
/// Sets the [`Document`]'s encoding..
fn set_encoding(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let (_, doc) = current!(cx.editor);
+ let doc = doc_mut!(cx.editor);
if let Some(label) = args.first() {
doc.set_encoding(label)
} else {
@@ -2486,7 +2638,7 @@ mod cmd {
/// Reload the [`Document`] from its source file.
fn reload(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let (view, doc) = current!(cx.editor);
@@ -2495,7 +2647,7 @@ mod cmd {
fn tree_sitter_scopes(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let (view, doc) = current!(cx.editor);
@@ -2509,15 +2661,18 @@ mod cmd {
fn vsplit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let id = view!(cx.editor).doc;
- if let Some(path) = args.get(0) {
- cx.editor.open(path.into(), Action::VerticalSplit)?;
- } else {
+ if args.is_empty() {
cx.editor.switch(id, Action::VerticalSplit);
+ } else {
+ for arg in args {
+ cx.editor
+ .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
+ }
}
Ok(())
@@ -2525,15 +2680,18 @@ mod cmd {
fn hsplit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let id = view!(cx.editor).doc;
- if let Some(path) = args.get(0) {
- cx.editor.open(path.into(), Action::HorizontalSplit)?;
- } else {
+ if args.is_empty() {
cx.editor.switch(id, Action::HorizontalSplit);
+ } else {
+ for arg in args {
+ cx.editor
+ .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
+ }
}
Ok(())
@@ -2541,7 +2699,7 @@ mod cmd {
fn debug_eval(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
if let Some(debugger) = cx.editor.debugger.as_mut() {
@@ -2563,7 +2721,7 @@ mod cmd {
fn debug_start(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let mut args = args.to_owned();
@@ -2571,12 +2729,12 @@ mod cmd {
0 => None,
_ => Some(args.remove(0)),
};
- dap_start_impl(cx, name, None, Some(args))
+ dap_start_impl(cx, name.as_deref(), None, Some(args))
}
fn debug_remote(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let mut args = args.to_owned();
@@ -2588,12 +2746,12 @@ mod cmd {
0 => None,
_ => Some(args.remove(0)),
};
- dap_start_impl(cx, name, address, Some(args))
+ dap_start_impl(cx, name.as_deref(), address, Some(args))
}
fn tutor(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let path = helix_core::runtime_dir().join("tutor.txt");
@@ -2605,20 +2763,135 @@ mod cmd {
pub(super) fn goto_line_number(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- if args.is_empty() {
- bail!("Line number required");
- }
+ ensure!(!args.is_empty(), "Line number required");
let line = args[0].parse::<usize>()?;
- goto_line_impl(&mut cx.editor, NonZeroUsize::new(line));
+ goto_line_impl(cx.editor, NonZeroUsize::new(line));
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, line);
+ Ok(())
+ }
+
+ fn setting(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let runtime_config = &mut cx.editor.config;
+
+ if args.len() != 2 {
+ anyhow::bail!("Bad arguments. Usage: `:set key field`");
+ }
+
+ let (key, arg) = (&args[0].to_lowercase(), &args[1]);
+
+ match key.as_ref() {
+ "scrolloff" => runtime_config.scrolloff = arg.parse()?,
+ "scroll-lines" => runtime_config.scroll_lines = arg.parse()?,
+ "mouse" => runtime_config.mouse = arg.parse()?,
+ "line-number" => runtime_config.line_number = arg.parse()?,
+ "middle-click_paste" => runtime_config.middle_click_paste = arg.parse()?,
+ "auto-pairs" => runtime_config.auto_pairs = arg.parse()?,
+ "auto-completion" => runtime_config.auto_completion = arg.parse()?,
+ "completion-trigger-len" => runtime_config.completion_trigger_len = arg.parse()?,
+ "auto-info" => runtime_config.auto_info = arg.parse()?,
+ "true-color" => runtime_config.true_color = arg.parse()?,
+ "search.smart-case" => runtime_config.search.smart_case = arg.parse()?,
+ "search.wrap-around" => runtime_config.search.wrap_around = arg.parse()?,
+ _ => anyhow::bail!("Unknown key `{}`.", args[0]),
+ }
+
+ Ok(())
+ }
+
+ fn sort(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ sort_impl(cx, args, false)
+ }
+
+ fn sort_reverse(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ sort_impl(cx, args, true)
+ }
+
+ fn sort_impl(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ reverse: bool,
+ ) -> anyhow::Result<()> {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+
+ let selection = doc.selection(view.id);
+
+ let mut fragments: Vec<_> = selection
+ .fragments(text)
+ .map(|fragment| Tendril::from(fragment.as_ref()))
+ .collect();
+
+ fragments.sort_by(match reverse {
+ true => |a: &Tendril, b: &Tendril| b.cmp(a),
+ false => |a: &Tendril, b: &Tendril| a.cmp(b),
+ });
+
+ let transaction = Transaction::change(
+ doc.text(),
+ selection
+ .into_iter()
+ .zip(fragments)
+ .map(|(s, fragment)| (s.from(), s.to(), Some(fragment))),
+ );
+
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+
+ Ok(())
+ }
+
+ fn tree_sitter_subtree(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let (view, doc) = current!(cx.editor);
+
+ if let Some(syntax) = doc.syntax() {
+ let primary_selection = doc.selection(view.id).primary();
+ let text = doc.text();
+ let from = text.char_to_byte(primary_selection.from());
+ let to = text.char_to_byte(primary_selection.to());
+ if let Some(selected_node) = syntax
+ .tree()
+ .root_node()
+ .descendant_for_byte_range(from, to)
+ {
+ let contents = format!("```tsq\n{}\n```", selected_node.to_sexp());
+
+ let callback = async move {
+ let call: job::Callback =
+ Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
+ let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
+ let popup = Popup::new("hover", contents);
+ compositor.replace_or_push("hover", Box::new(popup));
+ });
+ Ok(call)
+ };
+
+ cx.jobs.callback(callback);
+ }
+ }
Ok(())
}
@@ -2646,18 +2919,18 @@ mod cmd {
completer: Some(completers::filename),
},
TypableCommand {
- name: "buffer-close",
- aliases: &["bc", "bclose"],
- doc: "Close the current buffer.",
- fun: buffer_close,
- completer: None, // FIXME: buffer completer
+ name: "buffer-close",
+ aliases: &["bc", "bclose"],
+ doc: "Close the current buffer.",
+ fun: buffer_close,
+ completer: None, // FIXME: buffer completer
},
TypableCommand {
- name: "buffer-close!",
- aliases: &["bc!", "bclose!"],
- doc: "Close the current buffer forcefully (ignoring unsaved changes).",
- fun: force_buffer_close,
- completer: None, // FIXME: buffer completer
+ name: "buffer-close!",
+ aliases: &["bc!", "bclose!"],
+ doc: "Close the current buffer forcefully (ignoring unsaved changes).",
+ fun: force_buffer_close,
+ completer: None, // FIXME: buffer completer
},
TypableCommand {
name: "write",
@@ -2676,7 +2949,7 @@ mod cmd {
TypableCommand {
name: "format",
aliases: &["fmt"],
- doc: "Format the file using a formatter.",
+ doc: "Format the file using the LSP formatter.",
fun: format,
completer: None,
},
@@ -2765,9 +3038,16 @@ mod cmd {
completer: None,
},
TypableCommand {
+ name: "cquit!",
+ aliases: &["cq!"],
+ doc: "Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2).",
+ fun: force_cquit,
+ completer: None,
+ },
+ TypableCommand {
name: "theme",
aliases: &[],
- doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)",
+ doc: "Change the editor theme.",
fun: theme,
completer: Some(completers::theme),
},
@@ -2851,7 +3131,7 @@ mod cmd {
TypableCommand {
name: "change-current-directory",
aliases: &["cd"],
- doc: "Change the current working directory (:cd <dir>).",
+ doc: "Change the current working directory.",
fun: change_current_directory,
completer: Some(completers::directory),
},
@@ -2931,18 +3211,47 @@ mod cmd {
doc: "Go to line number.",
fun: goto_line_number,
completer: None,
- }
+ },
+ TypableCommand {
+ name: "set-option",
+ aliases: &["set"],
+ doc: "Set a config option at runtime",
+ fun: setting,
+ completer: Some(completers::setting),
+ },
+ TypableCommand {
+ name: "sort",
+ aliases: &[],
+ doc: "Sort ranges in selection.",
+ fun: sort,
+ completer: None,
+ },
+ TypableCommand {
+ name: "rsort",
+ aliases: &[],
+ doc: "Sort ranges in selection in reverse order.",
+ fun: sort_reverse,
+ completer: None,
+ },
+ TypableCommand {
+ name: "tree-sitter-subtree",
+ aliases: &["ts-subtree"],
+ doc: "Display tree sitter subtree under cursor, primarily for debugging queries.",
+ fun: tree_sitter_subtree,
+ completer: None,
+ },
];
- pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
- TYPABLE_COMMAND_LIST
- .iter()
- .flat_map(|cmd| {
- std::iter::once((cmd.name, cmd))
- .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
- })
- .collect()
- });
+ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
+ Lazy::new(|| {
+ TYPABLE_COMMAND_LIST
+ .iter()
+ .flat_map(|cmd| {
+ std::iter::once((cmd.name, cmd))
+ .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
+ })
+ .collect()
+ });
}
fn command_mode(cx: &mut Context) {
@@ -2950,17 +3259,28 @@ fn command_mode(cx: &mut Context) {
":".into(),
Some(':'),
|input: &str| {
+ static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
+ Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
+
// we use .this over split_whitespace() because we care about empty segments
let parts = input.split(' ').collect::<Vec<&str>>();
// simple heuristic: if there's no just one part, complete command name.
// if there's a space, per command completion kicks in.
if parts.len() <= 1 {
- let end = 0..;
- cmd::TYPABLE_COMMAND_LIST
+ let mut matches: Vec<_> = cmd::TYPABLE_COMMAND_LIST
.iter()
- .filter(|command| command.name.contains(input))
- .map(|command| (end.clone(), Cow::Borrowed(command.name)))
+ .filter_map(|command| {
+ FUZZY_MATCHER
+ .fuzzy_match(command.name, input)
+ .map(|score| (command.name, score))
+ })
+ .collect();
+
+ matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score));
+ matches
+ .into_iter()
+ .map(|(name, _)| (0.., name.into()))
.collect()
} else {
let part = parts.last().unwrap();
@@ -2968,7 +3288,7 @@ fn command_mode(cx: &mut Context) {
if let Some(cmd::TypableCommand {
completer: Some(completer),
..
- }) = cmd::COMMANDS.get(parts[0])
+ }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
{
completer(part)
.into_iter()
@@ -2996,15 +3316,25 @@ fn command_mode(cx: &mut Context) {
// If command is numeric, interpret as line number and go there.
if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
- if let Err(e) = cmd::goto_line_number(cx, &parts[0..], event) {
+ if let Err(e) = cmd::goto_line_number(cx, &[Cow::from(parts[0])], event) {
cx.editor.set_error(format!("{}", e));
}
return;
}
// Handle typable commands
- if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
- if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
+ if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
+ let args = if cfg!(unix) {
+ shellwords::shellwords(input)
+ } else {
+ // Windows doesn't support POSIX, so fallback for now
+ parts
+ .into_iter()
+ .map(|part| part.into())
+ .collect::<Vec<_>>()
+ };
+
+ if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
cx.editor.set_error(format!("{}", e));
}
} else {
@@ -3016,7 +3346,7 @@ fn command_mode(cx: &mut Context) {
prompt.doc_fn = Box::new(|input: &str| {
let part = input.split(' ').next().unwrap_or_default();
- if let Some(cmd::TypableCommand { doc, .. }) = cmd::COMMANDS.get(part) {
+ if let Some(cmd::TypableCommand { doc, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) {
return Some(doc);
}
@@ -3027,7 +3357,8 @@ fn command_mode(cx: &mut Context) {
}
fn file_picker(cx: &mut Context) {
- let root = find_root(None).unwrap_or_else(|| PathBuf::from("./"));
+ // We don't specify language markers, root will be the root of the current git repo
+ let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
let picker = ui::file_picker(root, &cx.editor.config);
cx.push_layer(Box::new(picker));
}
@@ -3084,8 +3415,8 @@ fn buffer_picker(cx: &mut Context) {
.map(|(_, doc)| new_meta(doc))
.collect(),
BufferMeta::format,
- |cx, meta, _action| {
- cx.editor.switch(meta.id, Action::Replace);
+ |cx, meta, action| {
+ cx.editor.switch(meta.id, action);
},
|editor, meta| {
let doc = &editor.documents.get(&meta.id)?;
@@ -3119,7 +3450,7 @@ fn symbol_picker(cx: &mut Context) {
nested_to_flat(list, file, child);
}
}
- let (_, doc) = current!(cx.editor);
+ let doc = doc!(cx.editor);
let language_server = match doc.language_server() {
Some(language_server) => language_server,
@@ -3140,7 +3471,7 @@ fn symbol_picker(cx: &mut Context) {
let symbols = match symbols {
lsp::DocumentSymbolResponse::Flat(symbols) => symbols,
lsp::DocumentSymbolResponse::Nested(symbols) => {
- let (_view, doc) = current!(editor);
+ let doc = doc!(editor);
let mut flat_symbols = Vec::new();
for symbol in symbols {
nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol)
@@ -3182,17 +3513,15 @@ fn symbol_picker(cx: &mut Context) {
}
fn workspace_symbol_picker(cx: &mut Context) {
- let (_, doc) = current!(cx.editor);
-
+ let doc = doc!(cx.editor);
+ let current_path = doc.path().cloned();
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => return,
};
let offset_encoding = language_server.offset_encoding();
-
let future = language_server.workspace_symbols("".to_string());
- let current_path = doc_mut!(cx.editor).path().cloned();
cx.callback(
future,
move |_editor: &mut Editor,
@@ -3243,6 +3572,15 @@ fn workspace_symbol_picker(cx: &mut Context) {
)
}
+impl ui::menu::Item for lsp::CodeActionOrCommand {
+ fn label(&self) -> &str {
+ match self {
+ lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(),
+ lsp::CodeActionOrCommand::Command(command) => command.title.as_str(),
+ }
+ }
+}
+
pub fn code_action(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
@@ -3262,49 +3600,85 @@ pub fn code_action(cx: &mut Context) {
cx.callback(
future,
- move |_editor: &mut Editor,
+ move |editor: &mut Editor,
compositor: &mut Compositor,
response: Option<lsp::CodeActionResponse>| {
- if let Some(actions) = response {
- let picker = Picker::new(
- true,
- actions,
- |action| match action {
- lsp::CodeActionOrCommand::CodeAction(action) => {
- action.title.as_str().into()
- }
- lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
- },
- move |cx, code_action, _action| match code_action {
- lsp::CodeActionOrCommand::Command(command) => {
- log::debug!("code action command: {:?}", command);
- cx.editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
+ let actions = match response {
+ Some(a) => a,
+ None => return,
+ };
+ if actions.is_empty() {
+ editor.set_status("No code actions available".to_owned());
+ return;
+ }
+
+ let mut picker = ui::Menu::new(actions, move |editor, code_action, event| {
+ if event != PromptEvent::Validate {
+ return;
+ }
+
+ // always present here
+ let code_action = code_action.unwrap();
+
+ match code_action {
+ lsp::CodeActionOrCommand::Command(command) => {
+ log::debug!("code action command: {:?}", command);
+ execute_lsp_command(editor, command.clone());
+ }
+ lsp::CodeActionOrCommand::CodeAction(code_action) => {
+ log::debug!("code action: {:?}", code_action);
+ if let Some(ref workspace_edit) = code_action.edit {
+ log::debug!("edit: {:?}", workspace_edit);
+ apply_workspace_edit(editor, offset_encoding, workspace_edit);
}
- lsp::CodeActionOrCommand::CodeAction(code_action) => {
- log::debug!("code action: {:?}", code_action);
- if let Some(ref workspace_edit) = code_action.edit {
- apply_workspace_edit(cx.editor, offset_encoding, workspace_edit)
- }
+
+ // if code action provides both edit and command first the edit
+ // should be applied and then the command
+ if let Some(command) = &code_action.command {
+ execute_lsp_command(editor, command.clone());
}
- },
- );
- compositor.push(Box::new(picker))
- }
+ }
+ }
+ });
+ picker.move_down(); // pre-select the first item
+
+ let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin {
+ vertical: 1,
+ horizontal: 1,
+ });
+ compositor.replace_or_push("code-action", Box::new(popup));
},
)
}
+pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
+ let doc = doc!(editor);
+ let language_server = match doc.language_server() {
+ Some(language_server) => language_server,
+ None => return,
+ };
+
+ // the command is executed on the server and communicated back
+ // to the client asynchronously using workspace edits
+ let command_future = language_server.command(cmd);
+ tokio::spawn(async move {
+ let res = command_future.await;
+
+ if let Err(e) = res {
+ log::error!("execute LSP command: {}", e);
+ }
+ });
+}
+
pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
use lsp::ResourceOp;
use std::fs;
match op {
ResourceOp::Create(op) => {
let path = op.uri.to_file_path().unwrap();
- let ignore_if_exists = if let Some(options) = &op.options {
+ let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
- } else {
- false
- };
+ });
if ignore_if_exists && path.exists() {
Ok(())
} else {
@@ -3314,11 +3688,12 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
ResourceOp::Delete(op) => {
let path = op.uri.to_file_path().unwrap();
if path.is_dir() {
- let recursive = if let Some(options) = &op.options {
- options.recursive.unwrap_or(false)
- } else {
- false
- };
+ let recursive = op
+ .options
+ .as_ref()
+ .and_then(|options| options.recursive)
+ .unwrap_or(false);
+
if recursive {
fs::remove_dir_all(&path)
} else {
@@ -3333,11 +3708,9 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
ResourceOp::Rename(op) => {
let from = op.old_uri.to_file_path().unwrap();
let to = op.new_uri.to_file_path().unwrap();
- let ignore_if_exists = if let Some(options) = &op.options {
+ let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
- } else {
- false
- };
+ });
if ignore_if_exists && to.exists() {
Ok(())
} else {
@@ -3347,7 +3720,7 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
}
}
-fn apply_workspace_edit(
+pub fn apply_workspace_edit(
editor: &mut Editor,
offset_encoding: OffsetEncoding,
workspace_edit: &lsp::WorkspaceEdit,
@@ -3454,7 +3827,7 @@ fn apply_workspace_edit(
fn last_picker(cx: &mut Context) {
// TODO: last picker does not seem to work well with buffer_picker
- cx.callback = Some(Box::new(|compositor: &mut Compositor| {
+ cx.callback = Some(Box::new(|compositor: &mut Compositor, _| {
if let Some(picker) = compositor.last_picker.take() {
compositor.push(picker);
}
@@ -3538,22 +3911,22 @@ fn open(cx: &mut Context, open: Open) {
let mut offs = 0;
let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
- let line = range.cursor_line(text);
+ let cursor_line = range.cursor_line(text);
- let line = match open {
+ let new_line = match open {
// adjust position to the end of the line (next line - 1)
- Open::Below => line + 1,
+ Open::Below => cursor_line + 1,
// adjust position to the end of the previous line (current line - 1)
- Open::Above => line,
+ Open::Above => cursor_line,
};
// Index to insert newlines after, as well as the char width
// to use to compensate for those inserted newlines.
- let (line_end_index, line_end_offset_width) = if line == 0 {
+ let (line_end_index, line_end_offset_width) = if new_line == 0 {
(0, 0)
} else {
(
- line_end_char_index(&doc.text().slice(..), line.saturating_sub(1)),
+ line_end_char_index(&doc.text().slice(..), new_line.saturating_sub(1)),
doc.line_ending.len_chars(),
)
};
@@ -3564,8 +3937,10 @@ fn open(cx: &mut Context, open: Open) {
doc.syntax(),
text,
line_end_index,
+ new_line.saturating_sub(1),
true,
- );
+ )
+ .unwrap_or_else(|| indent::indent_level_for_line(text.line(cursor_line), doc.tab_width()));
let indent = doc.indent_unit().repeat(indent_level);
let indent_len = indent.len();
let mut text = String::with_capacity(1 + indent_len);
@@ -3611,7 +3986,7 @@ fn normal_mode(cx: &mut Context) {
doc.mode = Mode::Normal;
- doc.append_changes_to_history(view.id);
+ try_restore_indent(doc, view.id);
// if leaving append mode, move cursor back by 1
if doc.restore_cursor {
@@ -3628,6 +4003,40 @@ fn normal_mode(cx: &mut Context) {
}
}
+fn try_restore_indent(doc: &mut Document, view_id: ViewId) {
+ use helix_core::chars::char_is_whitespace;
+ use helix_core::Operation;
+
+ fn inserted_a_new_blank_line(changes: &[Operation], pos: usize, line_end_pos: usize) -> bool {
+ if let [Operation::Retain(move_pos), Operation::Insert(ref inserted_str), Operation::Retain(_)] =
+ changes
+ {
+ move_pos + inserted_str.len() == pos
+ && inserted_str.starts_with('\n')
+ && inserted_str.chars().skip(1).all(char_is_whitespace)
+ && pos == line_end_pos // ensure no characters exists after current position
+ } else {
+ false
+ }
+ }
+
+ let doc_changes = doc.changes().changes();
+ let text = doc.text().slice(..);
+ let range = doc.selection(view_id).primary();
+ let pos = range.cursor(text);
+ let line_end_pos = line_end_char_index(&text, range.cursor_line(text));
+
+ if inserted_a_new_blank_line(doc_changes, pos, line_end_pos) {
+ // Removes tailing whitespaces.
+ let transaction =
+ Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
+ let line_start_pos = text.line_to_char(range.cursor_line(text));
+ (line_start_pos, pos, None)
+ });
+ doc.apply(&transaction, view_id);
+ }
+}
+
// Store a jump on the jumplist.
fn push_jump(editor: &mut Editor) {
let (view, doc) = current!(editor);
@@ -3636,7 +4045,7 @@ fn push_jump(editor: &mut Editor) {
}
fn goto_line(cx: &mut Context) {
- goto_line_impl(&mut cx.editor, cx.count)
+ goto_line_impl(cx.editor, cx.count)
}
fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
@@ -3702,6 +4111,20 @@ fn goto_last_modification(cx: &mut Context) {
}
}
+fn goto_last_modified_file(cx: &mut Context) {
+ let view = view!(cx.editor);
+ let alternate_file = view
+ .last_modified_docs
+ .into_iter()
+ .flatten()
+ .find(|&id| id != view.doc);
+ if let Some(alt) = alternate_file {
+ cx.editor.switch(alt, Action::Replace);
+ } else {
+ cx.editor.set_error("no last modified buffer".to_owned())
+ }
+}
+
fn select_mode(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
@@ -3979,27 +4402,21 @@ fn goto_pos(editor: &mut Editor, pos: usize) {
}
fn goto_first_diag(cx: &mut Context) {
- let editor = &mut cx.editor;
- let (_, doc) = current!(editor);
-
+ let doc = doc!(cx.editor);
let pos = match doc.diagnostics().first() {
Some(diag) => diag.range.start,
None => return,
};
-
- goto_pos(editor, pos);
+ goto_pos(cx.editor, pos);
}
fn goto_last_diag(cx: &mut Context) {
- let editor = &mut cx.editor;
- let (_, doc) = current!(editor);
-
+ let doc = doc!(cx.editor);
let pos = match doc.diagnostics().last() {
Some(diag) => diag.range.start,
None => return,
};
-
- goto_pos(editor, pos);
+ goto_pos(cx.editor, pos);
}
fn goto_next_diag(cx: &mut Context) {
@@ -4089,7 +4506,6 @@ fn signature_help(cx: &mut Context) {
);
}
-// NOTE: Transactions in this module get appended to history when we switch back to normal mode.
pub mod insert {
use super::*;
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
@@ -4184,8 +4600,10 @@ pub mod insert {
// The default insert hook: simply insert the character
#[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
- let t = Tendril::from_char(ch);
- let transaction = Transaction::insert(doc, selection, t);
+ let cursors = selection.clone().cursors(doc.slice(..));
+ let mut t = Tendril::new();
+ t.push(ch);
+ let transaction = Transaction::insert(doc, &cursors, t);
Some(transaction)
}
@@ -4200,11 +4618,11 @@ pub mod insert {
};
let text = doc.text();
- let selection = doc.selection(view.id).clone().cursors(text.slice(..));
+ let selection = doc.selection(view.id);
// run through insert hooks, stopping on the first one that returns Some(t)
for hook in hooks {
- if let Some(transaction) = hook(text, &selection, c) {
+ if let Some(transaction) = hook(text, selection, c) {
doc.apply(&transaction, view.id);
break;
}
@@ -4254,48 +4672,48 @@ pub mod insert {
};
let curr = contents.get_char(pos).unwrap_or(' ');
- // TODO: offset range.head by 1? when calculating?
+ let current_line = text.char_to_line(pos);
let indent_level = indent::suggested_indent_for_pos(
doc.language_config(),
doc.syntax(),
text,
- pos.saturating_sub(1),
+ pos,
+ current_line,
true,
- );
- let indent = doc.indent_unit().repeat(indent_level);
- let mut text = String::with_capacity(1 + indent.len());
- text.push_str(doc.line_ending.as_str());
- text.push_str(&indent);
+ )
+ .unwrap_or_else(|| {
+ indent::indent_level_for_line(text.line(current_line), doc.tab_width())
+ });
- let head = pos + offs + text.chars().count();
+ let indent = doc.indent_unit().repeat(indent_level);
+ let mut text = String::new();
+ // If we are between pairs (such as brackets), we want to insert an additional line which is indented one level more and place the cursor there
+ let new_head_pos = if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
+ let inner_indent = doc.indent_unit().repeat(indent_level + 1);
+ text.reserve_exact(2 + indent.len() + inner_indent.len());
+ text.push_str(doc.line_ending.as_str());
+ text.push_str(&inner_indent);
+ let new_head_pos = pos + offs + text.chars().count();
+ text.push_str(doc.line_ending.as_str());
+ text.push_str(&indent);
+ new_head_pos
+ } else {
+ text.reserve_exact(1 + indent.len());
+ text.push_str(doc.line_ending.as_str());
+ text.push_str(&indent);
+ pos + offs + text.chars().count()
+ };
// TODO: range replace or extend
// range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
// can be used with cx.mode to do replace or extend on most changes
- ranges.push(Range::new(
- if range.is_empty() {
- head
- } else {
- range.anchor + offs
- },
- head,
- ));
-
- // if between a bracket pair
- if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
- // another newline, indent the end bracket one level less
- let indent = doc.indent_unit().repeat(indent_level.saturating_sub(1));
- text.push_str(doc.line_ending.as_str());
- text.push_str(&indent);
- }
-
+ ranges.push(Range::new(new_head_pos, new_head_pos));
offs += text.chars().count();
(pos, pos, Some(text.into()))
});
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
- //
doc.apply(&transaction, view.id);
}
@@ -4519,11 +4937,8 @@ fn yank_joined_to_clipboard_impl(
fn yank_joined_to_clipboard(cx: &mut Context) {
let line_ending = doc!(cx.editor).line_ending;
- let _ = yank_joined_to_clipboard_impl(
- &mut cx.editor,
- line_ending.as_str(),
- ClipboardType::Clipboard,
- );
+ let _ =
+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Clipboard);
exit_select_mode(cx);
}
@@ -4548,20 +4963,17 @@ fn yank_main_selection_to_clipboard_impl(
}
fn yank_main_selection_to_clipboard(cx: &mut Context) {
- let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard);
}
fn yank_joined_to_primary_clipboard(cx: &mut Context) {
let line_ending = doc!(cx.editor).line_ending;
- let _ = yank_joined_to_clipboard_impl(
- &mut cx.editor,
- line_ending.as_str(),
- ClipboardType::Selection,
- );
+ let _ =
+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Selection);
}
fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
- let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection);
exit_select_mode(cx);
}
@@ -4576,11 +4988,12 @@ fn paste_impl(
doc: &mut Document,
view: &View,
action: Paste,
+ count: usize,
) -> Option<Transaction> {
let repeat = std::iter::repeat(
values
.last()
- .map(|value| Tendril::from_slice(value))
+ .map(|value| Tendril::from(value.repeat(count)))
.unwrap(),
);
@@ -4595,7 +5008,7 @@ fn paste_impl(
let mut values = values
.iter()
.map(|value| REGEX.replace_all(value, doc.line_ending.as_str()))
- .map(|value| Tendril::from(value.as_ref()))
+ .map(|value| Tendril::from(value.as_ref().repeat(count)))
.chain(repeat);
let text = doc.text();
@@ -4615,7 +5028,7 @@ fn paste_impl(
// paste append
(Paste::After, false) => range.to(),
};
- (pos, pos, Some(values.next().unwrap()))
+ (pos, pos, values.next())
});
Some(transaction)
@@ -4625,13 +5038,14 @@ fn paste_clipboard_impl(
editor: &mut Editor,
action: Paste,
clipboard_type: ClipboardType,
+ count: usize,
) -> anyhow::Result<()> {
let (view, doc) = current!(editor);
match editor
.clipboard_provider
.get_contents(clipboard_type)
- .map(|contents| paste_impl(&[contents], doc, view, action))
+ .map(|contents| paste_impl(&[contents], doc, view, action, count))
{
Ok(Some(transaction)) => {
doc.apply(&transaction, view.id);
@@ -4644,22 +5058,43 @@ fn paste_clipboard_impl(
}
fn paste_clipboard_after(cx: &mut Context) {
- let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard);
+ let _ = paste_clipboard_impl(
+ cx.editor,
+ Paste::After,
+ ClipboardType::Clipboard,
+ cx.count(),
+ );
}
fn paste_clipboard_before(cx: &mut Context) {
- let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Clipboard);
+ let _ = paste_clipboard_impl(
+ cx.editor,
+ Paste::Before,
+ ClipboardType::Clipboard,
+ cx.count(),
+ );
}
fn paste_primary_clipboard_after(cx: &mut Context) {
- let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection);
+ let _ = paste_clipboard_impl(
+ cx.editor,
+ Paste::After,
+ ClipboardType::Selection,
+ cx.count(),
+ );
}
fn paste_primary_clipboard_before(cx: &mut Context) {
- let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Selection);
+ let _ = paste_clipboard_impl(
+ cx.editor,
+ Paste::Before,
+ ClipboardType::Selection,
+ cx.count(),
+ );
}
fn replace_with_yanked(cx: &mut Context) {
+ let count = cx.count();
let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
@@ -4669,12 +5104,12 @@ fn replace_with_yanked(cx: &mut Context) {
let repeat = std::iter::repeat(
values
.last()
- .map(|value| Tendril::from_slice(value))
+ .map(|value| Tendril::from(&value.repeat(count)))
.unwrap(),
);
let mut values = values
.iter()
- .map(|value| Tendril::from_slice(value))
+ .map(|value| Tendril::from(&value.repeat(count)))
.chain(repeat);
let selection = doc.selection(view.id);
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
@@ -4686,7 +5121,6 @@ fn replace_with_yanked(cx: &mut Context) {
});
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
}
}
@@ -4694,6 +5128,7 @@ fn replace_with_yanked(cx: &mut Context) {
fn replace_selections_with_clipboard_impl(
editor: &mut Editor,
clipboard_type: ClipboardType,
+ count: usize,
) -> anyhow::Result<()> {
let (view, doc) = current!(editor);
@@ -4701,7 +5136,11 @@ fn replace_selections_with_clipboard_impl(
Ok(contents) => {
let selection = doc.selection(view.id);
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
- (range.from(), range.to(), Some(contents.as_str().into()))
+ (
+ range.from(),
+ range.to(),
+ Some(contents.repeat(count).as_str().into()),
+ )
});
doc.apply(&transaction, view.id);
@@ -4713,38 +5152,38 @@ fn replace_selections_with_clipboard_impl(
}
fn replace_selections_with_clipboard(cx: &mut Context) {
- let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
+ let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Clipboard, cx.count());
}
fn replace_selections_with_primary_clipboard(cx: &mut Context) {
- let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
+ let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Selection, cx.count());
}
fn paste_after(cx: &mut Context) {
+ let count = cx.count();
let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
if let Some(transaction) = registers
.read(reg_name)
- .and_then(|values| paste_impl(values, doc, view, Paste::After))
+ .and_then(|values| paste_impl(values, doc, view, Paste::After, count))
{
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
}
fn paste_before(cx: &mut Context) {
+ let count = cx.count();
let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
if let Some(transaction) = registers
.read(reg_name)
- .and_then(|values| paste_impl(values, doc, view, Paste::Before))
+ .and_then(|values| paste_impl(values, doc, view, Paste::Before, count))
{
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
}
@@ -4780,7 +5219,6 @@ fn indent(cx: &mut Context) {
}),
);
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
fn unindent(cx: &mut Context) {
@@ -4820,7 +5258,6 @@ fn unindent(cx: &mut Context) {
let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
fn format_selections(cx: &mut Context) {
@@ -4867,8 +5304,6 @@ fn format_selections(cx: &mut Context) {
// doc.apply(&transaction, view.id);
}
-
- doc.append_changes_to_history(view.id);
}
fn join_selections(cx: &mut Context) {
@@ -4911,7 +5346,6 @@ fn join_selections(cx: &mut Context) {
// .with_selection(selection);
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
@@ -5039,7 +5473,7 @@ pub fn completion(cx: &mut Context) {
move |editor: &mut Editor,
compositor: &mut Compositor,
response: Option<lsp::CompletionResponse>| {
- let (_, doc) = current!(editor);
+ let doc = doc!(editor);
if doc.mode() != Mode::Insert {
// we're not in insert mode anymore
return;
@@ -5136,9 +5570,10 @@ fn hover(cx: &mut Context) {
// skip if contents empty
- let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
- let popup = Popup::new(contents);
- compositor.push(Box::new(popup));
+ let contents =
+ ui::Markdown::new(contents, editor.syn_loader.clone()).style_group("hover");
+ let popup = Popup::new("hover", contents);
+ compositor.replace_or_push("hover", Box::new(popup));
}
},
);
@@ -5154,7 +5589,6 @@ fn toggle_comments(cx: &mut Context) {
let transaction = comment::toggle_line_comments(doc.text(), doc.selection(view.id), token);
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
exit_select_mode(cx);
}
@@ -5185,7 +5619,7 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
let selection = doc.selection(view.id);
let mut fragments: Vec<_> = selection
.fragments(text)
- .map(|fragment| Tendril::from_slice(&fragment))
+ .map(|fragment| Tendril::from(fragment.as_ref()))
.collect();
let group = count
@@ -5211,8 +5645,8 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
);
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
+
fn rotate_selection_contents_forward(cx: &mut Context) {
rotate_selection_contents(cx, Direction::Forward)
}
@@ -5228,14 +5662,73 @@ fn expand_selection(cx: &mut Context) {
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
- let selection = object::expand_selection(syntax, text, doc.selection(view.id));
+
+ let current_selection = doc.selection(view.id);
+
+ // save current selection so it can be restored using shrink_selection
+ view.object_selections.push(current_selection.clone());
+
+ let selection = object::expand_selection(syntax, text, current_selection.clone());
+ doc.set_selection(view.id, selection);
+ }
+ };
+ motion(cx.editor);
+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
+}
+
+fn shrink_selection(cx: &mut Context) {
+ let motion = |editor: &mut Editor| {
+ let (view, doc) = current!(editor);
+ let current_selection = doc.selection(view.id);
+ // try to restore previous selection
+ if let Some(prev_selection) = view.object_selections.pop() {
+ if current_selection.contains(&prev_selection) {
+ // allow shrinking the selection only if current selection contains the previous object selection
+ doc.set_selection(view.id, prev_selection);
+ return;
+ } else {
+ // clear existing selection as they can't be shrinked to anyway
+ view.object_selections.clear();
+ }
+ }
+ // if not previous selection, shrink to first child
+ if let Some(syntax) = doc.syntax() {
+ let text = doc.text().slice(..);
+ let selection = object::shrink_selection(syntax, text, current_selection.clone());
doc.set_selection(view.id, selection);
}
};
- motion(&mut cx.editor);
+ motion(cx.editor);
cx.editor.last_motion = Some(Motion(Box::new(motion)));
}
+fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
+where
+ F: Fn(Node) -> Option<Node>,
+{
+ let motion = |editor: &mut Editor| {
+ let (view, doc) = current!(editor);
+
+ if let Some(syntax) = doc.syntax() {
+ let text = doc.text().slice(..);
+ let current_selection = doc.selection(view.id);
+ let selection =
+ object::select_sibling(syntax, text, current_selection.clone(), sibling_fn);
+ doc.set_selection(view.id, selection);
+ }
+ };
+ motion(cx.editor);
+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
+}
+
+fn select_next_sibling(cx: &mut Context) {
+ select_sibling_impl(cx, &|node| Node::next_sibling(&node))
+}
+
+fn select_prev_sibling(cx: &mut Context) {
+ select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
+}
+
fn match_brackets(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
@@ -5288,6 +5781,12 @@ fn jump_backward(cx: &mut Context) {
};
}
+fn save_selection(cx: &mut Context) {
+ push_jump(cx.editor);
+ cx.editor
+ .set_status("Selection saved to jumplist".to_owned());
+}
+
fn rotate_view(cx: &mut Context) {
cx.editor.focus_next()
}
@@ -5358,8 +5857,10 @@ fn wonly(cx: &mut Context) {
}
fn select_register(cx: &mut Context) {
+ cx.editor.autoinfo = Some(Info::from_registers(&cx.editor.registers));
cx.on_next_key(move |cx, event| {
if let Some(ch) = event.char() {
+ cx.editor.autoinfo = None;
cx.editor.selected_register = Some(ch);
}
})
@@ -5464,7 +5965,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
});
doc.set_selection(view.id, selection);
};
- textobject(&mut cx.editor);
+ textobject(cx.editor);
cx.editor.last_motion = Some(Motion(Box::new(textobject)));
}
})
@@ -5479,13 +5980,16 @@ fn surround_add(cx: &mut Context) {
let mut changes = Vec::with_capacity(selection.len() * 2);
for range in selection.iter() {
- changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
- changes.push((range.to(), range.to(), Some(Tendril::from_char(close))));
+ let mut o = Tendril::new();
+ o.push(open);
+ let mut c = Tendril::new();
+ c.push(close);
+ changes.push((range.from(), range.from(), Some(o)));
+ changes.push((range.to(), range.to(), Some(c)));
}
let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
})
}
@@ -5510,15 +6014,12 @@ fn surround_replace(cx: &mut Context) {
let transaction = Transaction::change(
doc.text(),
change_pos.iter().enumerate().map(|(i, &pos)| {
- (
- pos,
- pos + 1,
- Some(Tendril::from_char(if i % 2 == 0 { open } else { close })),
- )
+ let mut t = Tendril::new();
+ t.push(if i % 2 == 0 { open } else { close });
+ (pos, pos + 1, Some(t))
}),
);
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
});
}
@@ -5541,7 +6042,6 @@ fn surround_delete(cx: &mut Context) {
let transaction =
Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
})
}
@@ -5630,9 +6130,7 @@ fn shell_impl(
) -> anyhow::Result<(Tendril, bool)> {
use std::io::Write;
use std::process::{Command, Stdio};
- if shell.is_empty() {
- bail!("No shell set");
- }
+ ensure!(!shell.is_empty(), "No shell set");
let mut process = match Command::new(&shell[0])
.args(&shell[1..])
@@ -5658,8 +6156,9 @@ fn shell_impl(
log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr));
}
- let tendril = Tendril::try_from_byte_slice(&output.stdout)
+ let str = std::str::from_utf8(&output.stdout)
.map_err(|_| anyhow!("Process did not output valid UTF-8"))?;
+ let tendril = Tendril::from(str);
Ok((tendril, output.status.success()))
}
@@ -5714,7 +6213,6 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
if behavior != ShellBehavior::Ignore {
let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
// after replace cursor may be out of bounds, do this to
@@ -5762,7 +6260,6 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
let transaction = Transaction::change(text, changes);
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
fn rename_symbol(cx: &mut Context) {
@@ -5796,7 +6293,7 @@ fn rename_symbol(cx: &mut Context) {
let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string());
let edits = block_on(task).unwrap_or_default();
log::debug!("Edits from LSP: {:?}", edits);
- apply_workspace_edit(&mut cx.editor, offset_encoding, &edits);
+ apply_workspace_edit(cx.editor, offset_encoding, &edits);
},
);
cx.push_layer(Box::new(prompt));
@@ -5816,16 +6313,45 @@ fn decrement(cx: &mut Context) {
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 text = doc.text().slice(..);
+
+ let changes: Vec<_> = selection
+ .ranges()
+ .iter()
+ .filter_map(|range| {
+ let incrementor: Box<dyn Increment> =
+ if let Some(incrementor) = DateTimeIncrementor::from_range(text, *range) {
+ Box::new(incrementor)
+ } else if let Some(incrementor) = NumberIncrementor::from_range(text, *range) {
+ Box::new(incrementor)
+ } else {
+ return None;
+ };
- 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),
- ))
+ let (range, new_text) = incrementor.increment(amount);
+
+ Some((range.from(), range.to(), Some(new_text)))
+ })
+ .collect();
+
+ // Overlapping changes in a transaction will panic, so we need to find and remove them.
+ // For example, if there are cursors on each of the year, month, and day of `2021-11-29`,
+ // incrementing will give overlapping changes, with each change incrementing a different part of
+ // the date. Since these conflict with each other we remove these changes from the transaction
+ // so nothing happens.
+ let mut overlapping_indexes = HashSet::new();
+ for (i, changes) in changes.windows(2).enumerate() {
+ if changes[0].1 > changes[1].0 {
+ overlapping_indexes.insert(i);
+ overlapping_indexes.insert(i + 1);
+ }
+ }
+ let changes = changes.into_iter().enumerate().filter_map(|(i, change)| {
+ if overlapping_indexes.contains(&i) {
+ None
+ } else {
+ Some(change)
+ }
});
if changes.clone().count() > 0 {
@@ -5833,6 +6359,58 @@ fn increment_impl(cx: &mut Context, amount: i64) {
let transaction = transaction.with_selection(selection.clone());
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
}
+
+fn record_macro(cx: &mut Context) {
+ if let Some((reg, mut keys)) = cx.editor.macro_recording.take() {
+ // Remove the keypress which ends the recording
+ keys.pop();
+ let s = keys
+ .into_iter()
+ .map(|key| {
+ let s = key.to_string();
+ if s.chars().count() == 1 {
+ s
+ } else {
+ format!("<{}>", s)
+ }
+ })
+ .collect::<String>();
+ cx.editor.registers.get_mut(reg).write(vec![s]);
+ cx.editor
+ .set_status(format!("Recorded to register [{}]", reg));
+ } else {
+ let reg = cx.register.take().unwrap_or('@');
+ cx.editor.macro_recording = Some((reg, Vec::new()));
+ cx.editor
+ .set_status(format!("Recording to register [{}]", reg));
+ }
+}
+
+fn replay_macro(cx: &mut Context) {
+ let reg = cx.register.unwrap_or('@');
+ let keys: Vec<KeyEvent> = if let Some([keys_str]) = cx.editor.registers.read(reg) {
+ match helix_view::input::parse_macro(keys_str) {
+ Ok(keys) => keys,
+ Err(err) => {
+ cx.editor.set_error(format!("Invalid macro: {}", err));
+ return;
+ }
+ }
+ } else {
+ cx.editor.set_error(format!("Register [{}] empty", reg));
+ return;
+ };
+
+ let count = cx.count();
+ cx.callback = Some(Box::new(
+ move |compositor: &mut Compositor, cx: &mut compositor::Context| {
+ for _ in 0..count {
+ for &key in keys.iter() {
+ compositor.handle_event(crossterm::event::Event::Key(key.into()), cx);
+ }
+ }
+ },
+ ));
+}
diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs
index 58ef99f5..c73f9611 100644
--- a/helix-term/src/commands/dap.rs
+++ b/helix-term/src/commands/dap.rs
@@ -194,7 +194,7 @@ pub fn dap_start_impl(
cx: &mut compositor::Context,
name: Option<&str>,
socket: Option<std::net::SocketAddr>,
- params: Option<Vec<&str>>,
+ params: Option<Vec<std::borrow::Cow<str>>>,
) -> Result<(), anyhow::Error> {
let doc = doc!(cx.editor);
@@ -242,7 +242,7 @@ pub fn dap_start_impl(
let mut param = x.to_string();
if let Some(DebugConfigCompletion::Advanced(cfg)) = template.completion.get(i) {
if matches!(cfg.completion.as_deref(), Some("filename" | "directory")) {
- param = std::fs::canonicalize(x)
+ param = std::fs::canonicalize(x.as_ref())
.ok()
.and_then(|pb| pb.into_os_string().into_string().ok())
.unwrap_or_else(|| x.to_string());
@@ -408,7 +408,7 @@ fn debug_parameter_prompt(
cx,
Some(&config_name),
None,
- Some(params.iter().map(|x| x.as_str()).collect()),
+ Some(params.iter().map(|x| x.into()).collect()),
) {
cx.editor.set_error(e.to_string());
}
@@ -651,7 +651,7 @@ pub fn dap_variables(cx: &mut Context) {
}
let contents = Text::from(tui::text::Text::from(variables));
- let popup = Popup::new(contents);
+ let popup = Popup::new("dap-variables", contents);
cx.push_layer(Box::new(popup));
}
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index 3a644750..dd7ebe1d 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -7,7 +7,7 @@ use helix_view::graphics::{CursorKind, Rect};
use crossterm::event::Event;
use tui::buffer::Buffer as Surface;
-pub type Callback = Box<dyn FnOnce(&mut Compositor)>;
+pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>;
// --> EventResult should have a callback that takes a context with methods like .popup(),
// .prompt() etc. That way we can abstract it from the renderer.
@@ -55,15 +55,20 @@ pub trait Component: Any + AnyComponent {
/// May be used by the parent component to compute the child area.
/// viewport is the maximum allowed area, and the child should stay within those bounds.
+ ///
+ /// The returned size might be larger than the viewport if the child is too big to fit.
+ /// In this case the parent can use the values to calculate scroll.
fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
- // TODO: for scrolling, the scroll wrapper should place a size + offset on the Context
- // that way render can use it
None
}
fn type_name(&self) -> &'static str {
std::any::type_name::<Self>()
}
+
+ fn id(&self) -> Option<&'static str> {
+ None
+ }
}
use anyhow::Error;
@@ -121,17 +126,32 @@ impl Compositor {
self.layers.push(layer);
}
+ /// Replace a component that has the given `id` with the new layer and if
+ /// no component is found, push the layer normally.
+ pub fn replace_or_push(&mut self, id: &'static str, layer: Box<dyn Component>) {
+ if let Some(component) = self.find_id(id) {
+ *component = layer;
+ } else {
+ self.push(layer)
+ }
+ }
+
pub fn pop(&mut self) -> Option<Box<dyn Component>> {
self.layers.pop()
}
pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
+ // If it is a key event and a macro is being recorded, push the key event to the recording.
+ if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {
+ keys.push(key.into());
+ }
+
// propagate events through the layers until we either find a layer that consumes it or we
// run out of layers (event bubbling)
for layer in self.layers.iter_mut().rev() {
match layer.handle_event(event, cx) {
EventResult::Consumed(Some(callback)) => {
- callback(self);
+ callback(self, cx);
return true;
}
EventResult::Consumed(None) => return true,
@@ -184,6 +204,14 @@ impl Compositor {
.find(|component| component.type_name() == type_name)
.and_then(|component| component.as_any_mut().downcast_mut())
}
+
+ pub fn find_id<T: 'static>(&mut self, id: &'static str) -> Option<&mut T> {
+ let type_name = std::any::type_name::<T>();
+ self.layers
+ .iter_mut()
+ .find(|component| component.type_name() == type_name && component.id() == Some(id))
+ .and_then(|component| component.as_any_mut().downcast_mut())
+ }
}
// View casting, taken straight from Cursive
diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs
index 3745f871..6b8bbc1b 100644
--- a/helix-term/src/config.rs
+++ b/helix-term/src/config.rs
@@ -20,14 +20,18 @@ pub struct LspConfig {
pub display_messages: bool,
}
-#[test]
-fn parsing_keymaps_config_file() {
- use crate::keymap;
- use crate::keymap::Keymap;
- use helix_core::hashmap;
- use helix_view::document::Mode;
-
- let sample_keymaps = r#"
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parsing_keymaps_config_file() {
+ use crate::keymap;
+ use crate::keymap::Keymap;
+ use helix_core::hashmap;
+ use helix_view::document::Mode;
+
+ let sample_keymaps = r#"
[keys.insert]
y = "move_line_down"
S-C-a = "delete_selection"
@@ -36,19 +40,20 @@ fn parsing_keymaps_config_file() {
A-F12 = "move_next_word_end"
"#;
- assert_eq!(
- toml::from_str::<Config>(sample_keymaps).unwrap(),
- Config {
- keys: Keymaps(hashmap! {
- Mode::Insert => Keymap::new(keymap!({ "Insert mode"
- "y" => move_line_down,
- "S-C-a" => delete_selection,
- })),
- Mode::Normal => Keymap::new(keymap!({ "Normal mode"
- "A-F12" => move_next_word_end,
- })),
- }),
- ..Default::default()
- }
- );
+ assert_eq!(
+ toml::from_str::<Config>(sample_keymaps).unwrap(),
+ Config {
+ keys: Keymaps(hashmap! {
+ Mode::Insert => Keymap::new(keymap!({ "Insert mode"
+ "y" => move_line_down,
+ "S-C-a" => delete_selection,
+ })),
+ Mode::Normal => Keymap::new(keymap!({ "Normal mode"
+ "A-F12" => move_next_word_end,
+ })),
+ }),
+ ..Default::default()
+ }
+ );
+ }
}
diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs
index 4fa38174..a6a77021 100644
--- a/helix-term/src/job.rs
+++ b/helix-term/src/job.rs
@@ -22,8 +22,8 @@ pub struct Jobs {
}
impl Job {
- pub fn new<F: Future<Output = anyhow::Result<()>> + Send + 'static>(f: F) -> Job {
- Job {
+ pub fn new<F: Future<Output = anyhow::Result<()>> + Send + 'static>(f: F) -> Self {
+ Self {
future: f.map(|r| r.map(|()| None)).boxed(),
wait: false,
}
@@ -31,22 +31,22 @@ impl Job {
pub fn with_callback<F: Future<Output = anyhow::Result<Callback>> + Send + 'static>(
f: F,
- ) -> Job {
- Job {
+ ) -> Self {
+ Self {
future: f.map(|r| r.map(Some)).boxed(),
wait: false,
}
}
- pub fn wait_before_exiting(mut self) -> Job {
+ pub fn wait_before_exiting(mut self) -> Self {
self.wait = true;
self
}
}
impl Jobs {
- pub fn new() -> Jobs {
- Jobs::default()
+ pub fn new() -> Self {
+ Self::default()
}
pub fn spawn<F: Future<Output = anyhow::Result<()>> + Send + 'static>(&mut self, f: F) {
@@ -93,8 +93,8 @@ impl Jobs {
}
/// Blocks until all the jobs that need to be waited on are done.
- pub fn finish(&mut self) {
+ pub async fn finish(&mut self) {
let wait_futures = std::mem::take(&mut self.wait_futures);
- helix_lsp::block_on(wait_futures.for_each(|_| future::ready(())));
+ wait_futures.for_each(|_| future::ready(())).await
}
}
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index b317242d..e08d7e44 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -1,4 +1,4 @@
-pub use crate::commands::Command;
+pub use crate::commands::MappableCommand;
use crate::config::Config;
use helix_core::hashmap;
use helix_view::{document::Mode, info::Info, input::KeyEvent};
@@ -92,7 +92,7 @@ macro_rules! alt {
#[macro_export]
macro_rules! keymap {
(@trie $cmd:ident) => {
- $crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd)
+ $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
};
(@trie
@@ -120,7 +120,7 @@ macro_rules! keymap {
_key,
keymap!(@trie $value)
);
- debug_assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
+ assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
_order.push(_key);
)+
)*
@@ -222,9 +222,8 @@ impl KeyTrieNode {
.map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
.collect();
}
- Info::new(self.name(), body)
+ Info::from_keymap(self.name(), body)
}
-
/// Get a reference to the key trie node's order.
pub fn order(&self) -> &[KeyEvent] {
self.order.as_slice()
@@ -260,8 +259,8 @@ impl DerefMut for KeyTrieNode {
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum KeyTrie {
- Leaf(Command),
- Sequence(Vec<Command>),
+ Leaf(MappableCommand),
+ Sequence(Vec<MappableCommand>),
Node(KeyTrieNode),
}
@@ -304,9 +303,9 @@ impl KeyTrie {
pub enum KeymapResultKind {
/// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode),
- Matched(Command),
+ Matched(MappableCommand),
/// Matched a sequence of commands to execute.
- MatchedSequence(Vec<Command>),
+ MatchedSequence(Vec<MappableCommand>),
/// Key was not found in the root keymap
NotFound,
/// Key is invalid in combination with previous keys. Contains keys leading upto
@@ -344,7 +343,7 @@ pub struct Keymap {
impl Keymap {
pub fn new(root: KeyTrie) -> Self {
- Keymap {
+ Self {
root,
state: Vec::new(),
sticky: None,
@@ -368,7 +367,7 @@ impl Keymap {
/// key cancels pending keystrokes. If there are no pending keystrokes but a
/// sticky node is in use, it will be cleared.
pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
- if let key!(Esc) = key {
+ if key!(Esc) == key {
if !self.state.is_empty() {
return KeymapResult::new(
// Note that Esc is not included here
@@ -386,10 +385,10 @@ impl Keymap {
};
let trie = match trie_node.search(&[*first]) {
- Some(&KeyTrie::Leaf(cmd)) => {
- return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
+ Some(KeyTrie::Leaf(ref cmd)) => {
+ return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky())
}
- Some(&KeyTrie::Sequence(ref cmds)) => {
+ Some(KeyTrie::Sequence(ref cmds)) => {
return KeymapResult::new(
KeymapResultKind::MatchedSequence(cmds.clone()),
self.sticky(),
@@ -408,9 +407,9 @@ impl Keymap {
}
KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
}
- Some(&KeyTrie::Leaf(cmd)) => {
+ Some(&KeyTrie::Leaf(ref cmd)) => {
self.state.clear();
- return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
+ return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky());
}
Some(&KeyTrie::Sequence(ref cmds)) => {
self.state.clear();
@@ -477,7 +476,7 @@ impl DerefMut for Keymaps {
}
impl Default for Keymaps {
- fn default() -> Keymaps {
+ fn default() -> Self {
let normal = keymap!({ "Normal mode"
"h" | "left" => move_char_left,
"j" | "down" => move_line_down,
@@ -521,9 +520,10 @@ impl Default for Keymaps {
"r" => goto_reference,
"i" => goto_implementation,
"t" => goto_window_top,
- "m" => goto_window_middle,
+ "c" => goto_window_center,
"b" => goto_window_bottom,
"a" => goto_last_accessed_file,
+ "m" => goto_last_modified_file,
"n" => goto_next_buffer,
"p" => goto_previous_buffer,
"." => goto_last_modification,
@@ -551,6 +551,11 @@ impl Default for Keymaps {
"S" => split_selection,
";" => collapse_selection,
"A-;" => flip_selections,
+ "A-k" => expand_selection,
+ "A-j" => shrink_selection,
+ "A-h" => select_prev_sibling,
+ "A-l" => select_next_sibling,
+
"%" => select_all,
"x" => extend_line,
"X" => extend_to_line_bounds,
@@ -592,6 +597,9 @@ impl Default for Keymaps {
// paste_all
"P" => paste_before,
+ "Q" => record_macro,
+ "q" => replay_macro,
+
">" => indent,
"<" => unindent,
"=" => format_selections,
@@ -613,6 +621,8 @@ impl Default for Keymaps {
"A-(" => rotate_selection_contents_backward,
"A-)" => rotate_selection_contents_forward,
+ "A-:" => ensure_selections_forward,
+
"esc" => normal_mode,
"C-b" | "pageup" => page_up,
"C-f" | "pagedown" => page_down,
@@ -640,7 +650,7 @@ impl Default for Keymaps {
"tab" => jump_forward, // tab == <C-i>
"C-o" => jump_backward,
- // "C-s" => save_selection,
+ "C-s" => save_selection,
"space" => { "Space"
"f" => file_picker,
@@ -763,8 +773,10 @@ impl Default for Keymaps {
"del" => delete_char_forward,
"C-d" => delete_char_forward,
"ret" => insert_newline,
+ "C-j" => insert_newline,
"tab" => insert_tab,
"C-w" => delete_word_backward,
+ "A-backspace" => delete_word_backward,
"A-d" => delete_word_forward,
"left" => move_char_left,
@@ -779,6 +791,8 @@ impl Default for Keymaps {
"A-left" => move_prev_word_end,
"A-f" => move_next_word_start,
"A-right" => move_next_word_start,
+ "A-<" => goto_file_start,
+ "A->" => goto_file_end,
"pageup" => page_up,
"pagedown" => page_down,
"home" => goto_line_start,
@@ -792,7 +806,7 @@ impl Default for Keymaps {
"C-x" => completion,
"C-r" => insert_register,
});
- Keymaps(hashmap!(
+ Self(hashmap!(
Mode::Normal => Keymap::new(normal),
Mode::Select => Keymap::new(select),
Mode::Insert => Keymap::new(insert),
@@ -852,36 +866,36 @@ mod tests {
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
assert_eq!(
keymap.get(key!('i')).kind,
- KeymapResultKind::Matched(Command::normal_mode),
+ KeymapResultKind::Matched(MappableCommand::normal_mode),
"Leaf should replace leaf"
);
assert_eq!(
keymap.get(key!('无')).kind,
- KeymapResultKind::Matched(Command::insert_mode),
+ KeymapResultKind::Matched(MappableCommand::insert_mode),
"New leaf should be present in merged keymap"
);
// Assumes that z is a node in the default keymap
assert_eq!(
keymap.get(key!('z')).kind,
- KeymapResultKind::Matched(Command::jump_backward),
+ KeymapResultKind::Matched(MappableCommand::jump_backward),
"Leaf should replace node"
);
// Assumes that `g` is a node in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
- &KeyTrie::Leaf(Command::goto_line_end),
+ &KeyTrie::Leaf(MappableCommand::goto_line_end),
"Leaf should be present in merged subnode"
);
// Assumes that `gg` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
- &KeyTrie::Leaf(Command::delete_char_forward),
+ &KeyTrie::Leaf(MappableCommand::delete_char_forward),
"Leaf should replace old leaf in merged subnode"
);
// Assumes that `ge` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
- &KeyTrie::Leaf(Command::goto_last_line),
+ &KeyTrie::Leaf(MappableCommand::goto_last_line),
"Old leaves in subnode should be present in merged node"
);
@@ -915,7 +929,7 @@ mod tests {
.root()
.search(&[key!(' '), key!('s'), key!('v')])
.unwrap(),
- &KeyTrie::Leaf(Command::vsplit),
+ &KeyTrie::Leaf(MappableCommand::vsplit),
"Leaf should be present in merged subnode"
);
// Make sure an order was set during merge
diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs
index f5e3a8cd..58cb139c 100644
--- a/helix-term/src/lib.rs
+++ b/helix-term/src/lib.rs
@@ -9,3 +9,14 @@ pub mod config;
pub mod job;
pub mod keymap;
pub mod ui;
+
+#[cfg(not(windows))]
+fn true_color() -> bool {
+ std::env::var("COLORTERM")
+ .map(|v| matches!(v.as_str(), "truecolor" | "24bit"))
+ .unwrap_or(false)
+}
+#[cfg(windows)]
+fn true_color() -> bool {
+ true
+}
diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs
index 88140130..0f504046 100644
--- a/helix-term/src/main.rs
+++ b/helix-term/src/main.rs
@@ -56,7 +56,7 @@ USAGE:
hx [FLAGS] [files]...
ARGS:
- <files>... Sets the input file to use
+ <files>... Sets the input file to use, position can also be specified via file[:row[:col]]
FLAGS:
-h, --help Prints help information
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index dd782d29..35afe81e 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -154,8 +154,19 @@ impl Completion {
);
doc.apply(&transaction, view.id);
- if let Some(additional_edits) = &item.additional_text_edits {
- // gopls uses this to add extra imports
+ // apply additional edits, mostly used to auto import unqualified types
+ let resolved_additional_text_edits = if item.additional_text_edits.is_some() {
+ None
+ } else {
+ Self::resolve_completion_item(doc, item.clone())
+ .and_then(|item| item.additional_text_edits)
+ };
+
+ if let Some(additional_edits) = item
+ .additional_text_edits
+ .as_ref()
+ .or_else(|| resolved_additional_text_edits.as_ref())
+ {
if !additional_edits.is_empty() {
let transaction = util::generate_transaction_from_edits(
doc.text(),
@@ -168,7 +179,7 @@ impl Completion {
}
};
});
- let popup = Popup::new(menu);
+ let popup = Popup::new("completion", menu);
let mut completion = Self {
popup,
start_offset,
@@ -181,6 +192,31 @@ impl Completion {
completion
}
+ fn resolve_completion_item(
+ doc: &Document,
+ completion_item: lsp::CompletionItem,
+ ) -> Option<CompletionItem> {
+ let language_server = doc.language_server()?;
+ let completion_resolve_provider = language_server
+ .capabilities()
+ .completion_provider
+ .as_ref()?
+ .resolve_provider;
+ if completion_resolve_provider != Some(true) {
+ return None;
+ }
+
+ let future = language_server.resolve_completion_item(completion_item);
+ let response = helix_lsp::block_on(future);
+ match response {
+ Ok(completion_item) => Some(completion_item),
+ Err(err) => {
+ log::error!("execute LSP command: {}", err);
+ None
+ }
+ }
+ }
+
pub fn recompute_filter(&mut self, editor: &Editor) {
// recompute menu based on matches
let menu = self.popup.contents_mut();
@@ -268,6 +304,9 @@ impl Component for Completion {
let cursor_pos = doc.selection(view.id).primary().cursor(text);
let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
let cursor_pos = (coords.row - view.offset.row) as u16;
+
+ let markdown_ui =
+ |content, syn_loader| Markdown::new(content, syn_loader).style_group("completion");
let mut markdown_doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
@@ -275,7 +314,7 @@ impl Component for Completion {
value: contents,
})) => {
// TODO: convert to wrapped text
- Markdown::new(
+ markdown_ui(
format!(
"```{}\n{}\n```\n{}",
language,
@@ -290,7 +329,7 @@ impl Component for Completion {
value: contents,
})) => {
// TODO: set language based on doc scope
- Markdown::new(
+ markdown_ui(
format!(
"```{}\n{}\n```\n{}",
language,
@@ -304,7 +343,7 @@ impl Component for Completion {
// TODO: copied from above
// TODO: set language based on doc scope
- Markdown::new(
+ markdown_ui(
format!(
"```{}\n{}\n```",
language,
@@ -328,8 +367,8 @@ impl Component for Completion {
let y = popup_y;
if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
- width = rel_width;
- height = rel_height;
+ width = rel_width.min(width);
+ height = rel_height.min(height);
}
Rect::new(x, y, width, height)
} else {
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index ac11d298..a2131abe 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -7,8 +7,10 @@ use crate::{
};
use helix_core::{
- coords_at_pos,
- graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary},
+ coords_at_pos, encoding,
+ graphemes::{
+ ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary,
+ },
movement::Direction,
syntax::{self, HighlightEvent},
unicode::segmentation::UnicodeSegmentation,
@@ -17,8 +19,8 @@ use helix_core::{
};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
+ editor::CursorShapeConfig,
graphics::{CursorKind, Modifier, Rect, Style},
- info::Info,
input::KeyEvent,
keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View,
@@ -31,10 +33,9 @@ use tui::buffer::Buffer as Surface;
pub struct EditorView {
keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
- last_insert: (commands::Command, Vec<KeyEvent>),
+ last_insert: (commands::MappableCommand, Vec<KeyEvent>),
pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners,
- autoinfo: Option<Info>,
}
impl Default for EditorView {
@@ -48,10 +49,9 @@ impl EditorView {
Self {
keymaps,
on_next_key: None,
- last_insert: (commands::Command::normal_mode, Vec::new()),
+ last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
- autoinfo: None,
}
}
@@ -106,13 +106,12 @@ impl EditorView {
}
}
- let highlights =
- Self::doc_syntax_highlights(doc, view.offset, inner.height, theme, &editor.syn_loader);
+ let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
Box::new(syntax::merge(
highlights,
- Self::doc_selection_highlights(doc, view, theme),
+ Self::doc_selection_highlights(doc, view, theme, &editor.config.cursor_shape),
))
} else {
Box::new(highlights)
@@ -130,8 +129,7 @@ impl EditorView {
let x = area.right();
let border_style = theme.get("ui.window");
for y in area.top()..area.bottom() {
- surface
- .get_mut(x, y)
+ surface[(x, y)]
.set_symbol(tui::symbols::line::VERTICAL)
//.set_symbol(" ")
.set_style(border_style);
@@ -154,8 +152,7 @@ impl EditorView {
doc: &'doc Document,
offset: Position,
height: u16,
- theme: &Theme,
- loader: &syntax::Loader,
+ _theme: &Theme,
) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
let text = doc.text().slice(..);
let last_line = std::cmp::min(
@@ -172,48 +169,34 @@ impl EditorView {
start..end
};
- // TODO: range doesn't actually restrict source, just highlight range
- let highlights = match doc.syntax() {
+ match doc.syntax() {
Some(syntax) => {
- let scopes = theme.scopes();
- syntax
- .highlight_iter(text.slice(..), Some(range), None, |language| {
- loader.language_configuration_for_injection_string(language)
- .and_then(|language_config| {
- let config = language_config.highlight_config(scopes)?;
- let config_ref = config.as_ref();
- // SAFETY: the referenced `HighlightConfiguration` behind
- // the `Arc` is guaranteed to remain valid throughout the
- // duration of the highlight.
- let config_ref = unsafe {
- std::mem::transmute::<
- _,
- &'static syntax::HighlightConfiguration,
- >(config_ref)
- };
- Some(config_ref)
- })
- })
+ let iter = syntax
+ // TODO: range doesn't actually restrict source, just highlight range
+ .highlight_iter(text.slice(..), Some(range), None)
.map(|event| event.unwrap())
- .collect() // TODO: we collect here to avoid holding the lock, fix later
+ .map(move |event| match event {
+ // convert byte offsets to char offset
+ HighlightEvent::Source { start, end } => {
+ let start =
+ text.byte_to_char(ensure_grapheme_boundary_next_byte(text, start));
+ let end =
+ text.byte_to_char(ensure_grapheme_boundary_next_byte(text, end));
+ HighlightEvent::Source { start, end }
+ }
+ event => event,
+ });
+
+ Box::new(iter)
}
- None => vec![HighlightEvent::Source {
- start: range.start,
- end: range.end,
- }],
+ None => Box::new(
+ [HighlightEvent::Source {
+ start: text.byte_to_char(range.start),
+ end: text.byte_to_char(range.end),
+ }]
+ .into_iter(),
+ ),
}
- .into_iter()
- .map(move |event| match event {
- // convert byte offsets to char offset
- HighlightEvent::Source { start, end } => {
- let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start));
- let end = ensure_grapheme_boundary_next(text, text.byte_to_char(end));
- HighlightEvent::Source { start, end }
- }
- event => event,
- });
-
- Box::new(highlights)
}
/// Get highlight spans for document diagnostics
@@ -245,11 +228,16 @@ impl EditorView {
doc: &Document,
view: &View,
theme: &Theme,
+ cursor_shape_config: &CursorShapeConfig,
) -> Vec<(usize, std::ops::Range<usize>)> {
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
let primary_idx = selection.primary_index();
+ let mode = doc.mode();
+ let cursorkind = cursor_shape_config.from_mode(mode);
+ let cursor_is_block = cursorkind == CursorKind::Block;
+
let selection_scope = theme
.find_scope_index("ui.selection")
.expect("could not find `ui.selection` scope in the theme!");
@@ -257,7 +245,7 @@ impl EditorView {
.find_scope_index("ui.cursor")
.unwrap_or(selection_scope);
- let cursor_scope = match doc.mode() {
+ let cursor_scope = match mode {
Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
Mode::Select => theme.find_scope_index("ui.cursor.select"),
Mode::Normal => Some(base_cursor_scope),
@@ -273,7 +261,8 @@ impl EditorView {
let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
for (i, range) in selection.iter().enumerate() {
- let (cursor_scope, selection_scope) = if i == primary_idx {
+ let selection_is_primary = i == primary_idx;
+ let (cursor_scope, selection_scope) = if selection_is_primary {
(primary_cursor_scope, primary_selection_scope)
} else {
(cursor_scope, selection_scope)
@@ -281,7 +270,14 @@ impl EditorView {
// Special-case: cursor at end of the rope.
if range.head == range.anchor && range.head == text.len_chars() {
- spans.push((cursor_scope, range.head..range.head + 1));
+ if !selection_is_primary || cursor_is_block {
+ // Bar and underline cursors are drawn by the terminal
+ // BUG: If the editor area loses focus while having a bar or
+ // underline cursor (eg. when a regex prompt has focus) then
+ // the primary cursor will be invisible. This doesn't happen
+ // with block cursors since we manually draw *all* cursors.
+ spans.push((cursor_scope, range.head..range.head + 1));
+ }
continue;
}
@@ -290,11 +286,15 @@ impl EditorView {
// Standard case.
let cursor_start = prev_grapheme_boundary(text, range.head);
spans.push((selection_scope, range.anchor..cursor_start));
- spans.push((cursor_scope, cursor_start..range.head));
+ if !selection_is_primary || cursor_is_block {
+ spans.push((cursor_scope, cursor_start..range.head));
+ }
} else {
// Reverse case.
let cursor_end = next_grapheme_boundary(text, range.head);
- spans.push((cursor_scope, range.head..cursor_end));
+ if !selection_is_primary || cursor_is_block {
+ spans.push((cursor_scope, range.head..cursor_end));
+ }
spans.push((selection_scope, cursor_end..range.anchor));
}
}
@@ -320,6 +320,10 @@ impl EditorView {
let text_style = theme.get("ui.text");
+ // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch
+ // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
+ let text = text.slice(..);
+
'outer: for event in highlights {
match event {
HighlightEvent::HighlightStart(span) => {
@@ -336,17 +340,16 @@ impl EditorView {
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
- let style = spans.iter().fold(text_style, |acc, span| {
- let style = theme.get(theme.scopes()[span.0].as_str());
- acc.patch(style)
- });
-
for grapheme in RopeGraphemes::new(text) {
let out_of_bounds = visual_x < offset.col as u16
|| visual_x >= viewport.width + offset.col as u16;
if LineEnding::from_rope_slice(&grapheme).is_some() {
if !out_of_bounds {
+ let style = spans.iter().fold(text_style, |acc, span| {
+ acc.patch(theme.highlight(span.0))
+ });
+
// we still want to render an empty cell with the style
surface.set_string(
viewport.x + visual_x - offset.col as u16,
@@ -377,6 +380,10 @@ impl EditorView {
};
if !out_of_bounds {
+ let style = spans.iter().fold(text_style, |acc, span| {
+ acc.patch(theme.highlight(span.0))
+ });
+
// if we're offscreen just keep going until we hit a new line
surface.set_string(
viewport.x + visual_x - offset.col as u16,
@@ -422,8 +429,7 @@ impl EditorView {
.add_modifier(Modifier::DIM)
});
- surface
- .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16)
+ surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)]
.set_style(style);
}
}
@@ -453,6 +459,8 @@ impl EditorView {
let mut offset = 0;
+ let gutter_style = theme.get("ui.gutter");
+
// avoid lots of small allocations by reusing a text buffer for each line
let mut text = String::with_capacity(8);
@@ -468,7 +476,7 @@ impl EditorView {
viewport.y + i as u16,
&text,
*width,
- style,
+ gutter_style.patch(style),
);
}
text.clear();
@@ -574,21 +582,6 @@ impl EditorView {
}
surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
- let rel_path = doc.relative_path();
- let path = rel_path
- .as_ref()
- .map(|p| p.to_string_lossy())
- .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
-
- let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" });
- surface.set_stringn(
- viewport.x + 8,
- viewport.y,
- title,
- viewport.width.saturating_sub(6) as usize,
- base_style,
- );
-
//-------------------------------
// Right side of the status line.
//-------------------------------
@@ -662,6 +655,13 @@ impl EditorView {
base_style,
));
+ let enc = doc.encoding();
+ if enc != encoding::UTF_8 {
+ right_side_text
+ .0
+ .push(Span::styled(format!(" {} ", enc.name()), base_style));
+ }
+
// Render to the statusline.
surface.set_spans(
viewport.x
@@ -672,6 +672,31 @@ impl EditorView {
&right_side_text,
right_side_text.width() as u16,
);
+
+ //-------------------------------
+ // Middle / File path / Title
+ //-------------------------------
+ let title = {
+ let rel_path = doc.relative_path();
+ let path = rel_path
+ .as_ref()
+ .map(|p| p.to_string_lossy())
+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
+ format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" })
+ };
+
+ surface.set_string_truncated(
+ viewport.x + 8, // 8: 1 space + 3 char mode string + 1 space + 1 spinner + 1 space
+ viewport.y,
+ title,
+ viewport
+ .width
+ .saturating_sub(6)
+ .saturating_sub(right_side_text.width() as u16 + 1) as usize, // "+ 1": a space between the title and the selection info
+ base_style,
+ true,
+ true,
+ );
}
/// Handle events by looking them up in `self.keymaps`. Returns None
@@ -684,12 +709,13 @@ impl EditorView {
cxt: &mut commands::Context,
event: KeyEvent,
) -> Option<KeymapResult> {
+ cxt.editor.autoinfo = None;
let key_result = self.keymaps.get_mut(&mode).unwrap().get(event);
- self.autoinfo = key_result.sticky.map(|node| node.infobox());
+ cxt.editor.autoinfo = key_result.sticky.map(|node| node.infobox());
match &key_result.kind {
KeymapResultKind::Matched(command) => command.execute(cxt),
- KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
+ KeymapResultKind::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()),
KeymapResultKind::MatchedSequence(commands) => {
for command in commands {
command.execute(cxt);
@@ -789,8 +815,9 @@ impl EditorView {
pub fn clear_completion(&mut self, editor: &mut Editor) {
self.completion = None;
+
// Clear any savepoints
- let (_, doc) = current!(editor);
+ let doc = doc_mut!(editor);
doc.savepoint = None;
editor.clear_idle_timer(); // don't retrigger
}
@@ -927,7 +954,7 @@ impl EditorView {
return EventResult::Ignored;
}
- commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt);
+ commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt);
EventResult::Consumed(None)
}
@@ -953,9 +980,9 @@ impl EditorView {
if let Ok(pos) = doc.text().try_line_to_char(line) {
doc.set_selection(view_id, Selection::point(pos));
if modifiers == crossterm::event::KeyModifiers::ALT {
- commands::Command::dap_edit_log.execute(cxt);
+ commands::MappableCommand::dap_edit_log.execute(cxt);
} else {
- commands::Command::dap_edit_condition.execute(cxt);
+ commands::MappableCommand::dap_edit_condition.execute(cxt);
}
return EventResult::Consumed(None);
@@ -977,7 +1004,8 @@ impl EditorView {
}
if modifiers == crossterm::event::KeyModifiers::ALT {
- commands::Command::replace_selections_with_primary_clipboard.execute(cxt);
+ commands::MappableCommand::replace_selections_with_primary_clipboard
+ .execute(cxt);
return EventResult::Consumed(None);
}
@@ -991,7 +1019,7 @@ impl EditorView {
let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
doc.set_selection(view_id, Selection::point(pos));
editor.tree.focus = view_id;
- commands::Command::paste_primary_clipboard_before.execute(cxt);
+ commands::MappableCommand::paste_primary_clipboard_before.execute(cxt);
return EventResult::Consumed(None);
}
@@ -1004,14 +1032,18 @@ impl EditorView {
}
impl Component for EditorView {
- fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
- let mut cxt = commands::Context {
- editor: &mut cx.editor,
+ fn handle_event(
+ &mut self,
+ event: Event,
+ context: &mut crate::compositor::Context,
+ ) -> EventResult {
+ let mut cx = commands::Context {
+ editor: context.editor,
count: None,
register: None,
callback: None,
on_next_key_callback: None,
- jobs: cx.jobs,
+ jobs: context.jobs,
};
match event {
@@ -1021,18 +1053,19 @@ impl Component for EditorView {
EventResult::Consumed(None)
}
Event::Key(key) => {
- cxt.editor.reset_idle_timer();
+ cx.editor.reset_idle_timer();
let mut key = KeyEvent::from(key);
canonicalize_key(&mut key);
+
// clear status
- cxt.editor.status_msg = None;
+ cx.editor.status_msg = None;
- let (_, doc) = current!(cxt.editor);
+ let doc = doc!(cx.editor);
let mode = doc.mode();
if let Some(on_next_key) = self.on_next_key.take() {
// if there's a command waiting input, do that first
- on_next_key(&mut cxt, key);
+ on_next_key(&mut cx, key);
} else {
match mode {
Mode::Insert => {
@@ -1044,8 +1077,8 @@ impl Component for EditorView {
if let Some(completion) = &mut self.completion {
// use a fake context here
let mut cx = Context {
- editor: cxt.editor,
- jobs: cxt.jobs,
+ editor: cx.editor,
+ jobs: cx.jobs,
scroll: None,
};
let res = completion.handle_event(event, &mut cx);
@@ -1055,40 +1088,46 @@ impl Component for EditorView {
if callback.is_some() {
// assume close_fn
- self.clear_completion(cxt.editor);
+ self.clear_completion(cx.editor);
}
}
}
// if completion didn't take the event, we pass it onto commands
if !consumed {
- self.insert_mode(&mut cxt, key);
+ self.insert_mode(&mut cx, key);
// lastly we recalculate completion
if let Some(completion) = &mut self.completion {
- completion.update(&mut cxt);
+ completion.update(&mut cx);
if completion.is_empty() {
- self.clear_completion(cxt.editor);
+ self.clear_completion(cx.editor);
}
}
}
}
- mode => self.command_mode(mode, &mut cxt, key),
+ mode => self.command_mode(mode, &mut cx, key),
}
}
- self.on_next_key = cxt.on_next_key_callback.take();
+ self.on_next_key = cx.on_next_key_callback.take();
// appease borrowck
- let callback = cxt.callback.take();
+ let callback = cx.callback.take();
// if the command consumed the last view, skip the render.
// on the next loop cycle the Application will then terminate.
- if cxt.editor.should_close() {
+ if cx.editor.should_close() {
return EventResult::Ignored;
}
- let (view, doc) = current!(cxt.editor);
- view.ensure_cursor_in_view(doc, cxt.editor.config.scrolloff);
+ let (view, doc) = current!(cx.editor);
+ view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
+
+ // Store a history state if not in insert mode. This also takes care of
+ // commiting changes when leaving insert mode.
+ if doc.mode() != Mode::Insert {
+ doc.append_changes_to_history(view.id);
+ }
// mode transitions
match (mode, doc.mode()) {
@@ -1117,7 +1156,7 @@ impl Component for EditorView {
EventResult::Consumed(callback)
}
- Event::Mouse(event) => self.handle_mouse_event(event, &mut cxt),
+ Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
}
}
@@ -1134,8 +1173,9 @@ impl Component for EditorView {
}
if cx.editor.config.auto_info {
- if let Some(ref mut info) = self.autoinfo {
+ if let Some(mut info) = cx.editor.autoinfo.take() {
info.render(area, surface, cx);
+ cx.editor.autoinfo = Some(info)
}
}
@@ -1173,13 +1213,31 @@ impl Component for EditorView {
disp.push_str(&s);
}
}
+ let style = cx.editor.theme.get("ui.text");
+ let macro_width = if cx.editor.macro_recording.is_some() {
+ 3
+ } else {
+ 0
+ };
surface.set_string(
- area.x + area.width.saturating_sub(key_width),
+ area.x + area.width.saturating_sub(key_width + macro_width),
area.y + area.height.saturating_sub(1),
disp.get(disp.len().saturating_sub(key_width as usize)..)
.unwrap_or(&disp),
- cx.editor.theme.get("ui.text"),
+ style,
);
+ if let Some((reg, _)) = cx.editor.macro_recording {
+ let disp = format!("[{}]", reg);
+ let style = style
+ .fg(helix_view::graphics::Color::Yellow)
+ .add_modifier(Modifier::BOLD);
+ surface.set_string(
+ area.x + area.width.saturating_sub(3),
+ area.y + area.height.saturating_sub(1),
+ &disp,
+ style,
+ );
+ }
}
if let Some(completion) = self.completion.as_mut() {
@@ -1188,11 +1246,11 @@ impl Component for EditorView {
}
fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
- // match view.doc.mode() {
- // Mode::Insert => write!(stdout, "\x1B[6 q"),
- // mode => write!(stdout, "\x1B[2 q"),
- // };
- editor.cursor()
+ match editor.cursor() {
+ // All block cursors are drawn manually
+ (pos, CursorKind::Block) => (pos, CursorKind::Hidden),
+ cursor => cursor,
+ }
}
}
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index ca8303dd..6a7b641a 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -21,6 +21,9 @@ pub struct Markdown {
contents: String,
config_loader: Arc<syntax::Loader>,
+
+ block_style: String,
+ heading_style: String,
}
// TODO: pre-render and self reference via Pin
@@ -31,120 +34,137 @@ impl Markdown {
Self {
contents,
config_loader,
+ block_style: "markup.raw.inline".into(),
+ heading_style: "markup.heading".into(),
}
}
-}
-fn parse<'a>(
- contents: &'a str,
- theme: Option<&Theme>,
- loader: &syntax::Loader,
-) -> tui::text::Text<'a> {
- // // also 2021-03-04T16:33:58.553 helix_lsp::transport [INFO] <- {"contents":{"kind":"markdown","value":"\n```rust\ncore::num\n```\n\n```rust\npub const fn saturating_sub(self, rhs:Self) ->Self\n```\n\n---\n\n```rust\n```"},"range":{"end":{"character":61,"line":101},"start":{"character":47,"line":101}}}
- // let text = "\n```rust\ncore::iter::traits::iterator::Iterator\n```\n\n```rust\nfn collect<B: FromIterator<Self::Item>>(self) -> B\nwhere\n Self: Sized,\n```\n\n---\n\nTransforms an iterator into a collection.\n\n`collect()` can take anything iterable, and turn it into a relevant\ncollection. This is one of the more powerful methods in the standard\nlibrary, used in a variety of contexts.\n\nThe most basic pattern in which `collect()` is used is to turn one\ncollection into another. You take a collection, call [`iter`](https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html) on it,\ndo a bunch of transformations, and then `collect()` at the end.\n\n`collect()` can also create instances of types that are not typical\ncollections. For example, a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html) can be built from [`char`](type@char)s,\nand an iterator of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html) items can be collected\ninto `Result<Collection<T>, E>`. See the examples below for more.\n\nBecause `collect()` is so general, it can cause problems with type\ninference. As such, `collect()` is one of the few times you'll see\nthe syntax affectionately known as the 'turbofish': `::<>`. This\nhelps the inference algorithm understand specifically which collection\nyou're trying to collect into.\n\n# Examples\n\nBasic usage:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled: Vec<i32> = a.iter()\n .map(|&x| x * 2)\n .collect();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nNote that we needed the `: Vec<i32>` on the left-hand side. This is because\nwe could collect into, for example, a [`VecDeque<T>`](https://doc.rust-lang.org/nightly/core/iter/std/collections/struct.VecDeque.html) instead:\n\n```rust\nuse std::collections::VecDeque;\n\nlet a = [1, 2, 3];\n\nlet doubled: VecDeque<i32> = a.iter().map(|&x| x * 2).collect();\n\nassert_eq!(2, doubled[0]);\nassert_eq!(4, doubled[1]);\nassert_eq!(6, doubled[2]);\n```\n\nUsing the 'turbofish' instead of annotating `doubled`:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<i32>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nBecause `collect()` only cares about what you're collecting into, you can\nstill use a partial type hint, `_`, with the turbofish:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<_>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nUsing `collect()` to make a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html):\n\n```rust\nlet chars = ['g', 'd', 'k', 'k', 'n'];\n\nlet hello: String = chars.iter()\n .map(|&x| x as u8)\n .map(|x| (x + 1) as char)\n .collect();\n\nassert_eq!(\"hello\", hello);\n```\n\nIf you have a list of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html)s, you can use `collect()` to\nsee if any of them failed:\n\n```rust\nlet results = [Ok(1), Err(\"nope\"), Ok(3), Err(\"bad\")];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the first error\nassert_eq!(Err(\"nope\"), result);\n\nlet results = [Ok(1), Ok(3)];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the list of answers\nassert_eq!(Ok(vec![1, 3]), result);\n```";
-
- let mut options = Options::empty();
- options.insert(Options::ENABLE_STRIKETHROUGH);
- let parser = Parser::new_ext(contents, options);
-
- // TODO: if possible, render links as terminal hyperlinks: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
- let mut tags = Vec::new();
- let mut spans = Vec::new();
- let mut lines = Vec::new();
-
- fn to_span(text: pulldown_cmark::CowStr) -> Span {
- use std::ops::Deref;
- Span::raw::<std::borrow::Cow<_>>(match text {
- CowStr::Borrowed(s) => s.into(),
- CowStr::Boxed(s) => s.to_string().into(),
- CowStr::Inlined(s) => s.deref().to_owned().into(),
- })
+ pub fn style_group(mut self, suffix: &str) -> Self {
+ self.block_style = format!("markup.raw.inline.{}", suffix);
+ self.heading_style = format!("markup.heading.{}", suffix);
+ self
}
- let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default();
-
- // TODO: use better scopes for these, `markup.raw.block`, `markup.heading`
- let code_style = theme
- .map(|theme| theme.get("ui.text.focus"))
- .unwrap_or_default(); // white
- let heading_style = theme
- .map(|theme| theme.get("ui.linenr.selected"))
- .unwrap_or_default(); // lilac
-
- for event in parser {
- match event {
- Event::Start(tag) => tags.push(tag),
- Event::End(tag) => {
- tags.pop();
- match tag {
- Tag::Heading(_) | Tag::Paragraph | Tag::CodeBlock(CodeBlockKind::Fenced(_)) => {
- // whenever code block or paragraph closes, new line
- let spans = std::mem::take(&mut spans);
- if !spans.is_empty() {
- lines.push(Spans::from(spans));
+ fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
+ // // also 2021-03-04T16:33:58.553 helix_lsp::transport [INFO] <- {"contents":{"kind":"markdown","value":"\n```rust\ncore::num\n```\n\n```rust\npub const fn saturating_sub(self, rhs:Self) ->Self\n```\n\n---\n\n```rust\n```"},"range":{"end":{"character":61,"line":101},"start":{"character":47,"line":101}}}
+ // let text = "\n```rust\ncore::iter::traits::iterator::Iterator\n```\n\n```rust\nfn collect<B: FromIterator<Self::Item>>(self) -> B\nwhere\n Self: Sized,\n```\n\n---\n\nTransforms an iterator into a collection.\n\n`collect()` can take anything iterable, and turn it into a relevant\ncollection. This is one of the more powerful methods in the standard\nlibrary, used in a variety of contexts.\n\nThe most basic pattern in which `collect()` is used is to turn one\ncollection into another. You take a collection, call [`iter`](https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html) on it,\ndo a bunch of transformations, and then `collect()` at the end.\n\n`collect()` can also create instances of types that are not typical\ncollections. For example, a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html) can be built from [`char`](type@char)s,\nand an iterator of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html) items can be collected\ninto `Result<Collection<T>, E>`. See the examples below for more.\n\nBecause `collect()` is so general, it can cause problems with type\ninference. As such, `collect()` is one of the few times you'll see\nthe syntax affectionately known as the 'turbofish': `::<>`. This\nhelps the inference algorithm understand specifically which collection\nyou're trying to collect into.\n\n# Examples\n\nBasic usage:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled: Vec<i32> = a.iter()\n .map(|&x| x * 2)\n .collect();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nNote that we needed the `: Vec<i32>` on the left-hand side. This is because\nwe could collect into, for example, a [`VecDeque<T>`](https://doc.rust-lang.org/nightly/core/iter/std/collections/struct.VecDeque.html) instead:\n\n```rust\nuse std::collections::VecDeque;\n\nlet a = [1, 2, 3];\n\nlet doubled: VecDeque<i32> = a.iter().map(|&x| x * 2).collect();\n\nassert_eq!(2, doubled[0]);\nassert_eq!(4, doubled[1]);\nassert_eq!(6, doubled[2]);\n```\n\nUsing the 'turbofish' instead of annotating `doubled`:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<i32>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nBecause `collect()` only cares about what you're collecting into, you can\nstill use a partial type hint, `_`, with the turbofish:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<_>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nUsing `collect()` to make a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html):\n\n```rust\nlet chars = ['g', 'd', 'k', 'k', 'n'];\n\nlet hello: String = chars.iter()\n .map(|&x| x as u8)\n .map(|x| (x + 1) as char)\n .collect();\n\nassert_eq!(\"hello\", hello);\n```\n\nIf you have a list of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html)s, you can use `collect()` to\nsee if any of them failed:\n\n```rust\nlet results = [Ok(1), Err(\"nope\"), Ok(3), Err(\"bad\")];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the first error\nassert_eq!(Err(\"nope\"), result);\n\nlet results = [Ok(1), Ok(3)];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the list of answers\nassert_eq!(Ok(vec![1, 3]), result);\n```";
+
+ let mut options = Options::empty();
+ options.insert(Options::ENABLE_STRIKETHROUGH);
+ let parser = Parser::new_ext(&self.contents, options);
+
+ // TODO: if possible, render links as terminal hyperlinks: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
+ let mut tags = Vec::new();
+ let mut spans = Vec::new();
+ let mut lines = Vec::new();
+
+ fn to_span(text: pulldown_cmark::CowStr) -> Span {
+ use std::ops::Deref;
+ Span::raw::<std::borrow::Cow<_>>(match text {
+ CowStr::Borrowed(s) => s.into(),
+ CowStr::Boxed(s) => s.to_string().into(),
+ CowStr::Inlined(s) => s.deref().to_owned().into(),
+ })
+ }
+
+ macro_rules! get_theme {
+ ($s1: expr) => {
+ theme
+ .map(|theme| theme.try_get($s1.as_str()))
+ .flatten()
+ .unwrap_or_default()
+ };
+ }
+ let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default();
+ let code_style = get_theme!(self.block_style);
+ let heading_style = get_theme!(self.heading_style);
+
+ for event in parser {
+ match event {
+ Event::Start(tag) => tags.push(tag),
+ Event::End(tag) => {
+ tags.pop();
+ match tag {
+ Tag::Heading(_, _, _)
+ | Tag::Paragraph
+ | Tag::CodeBlock(CodeBlockKind::Fenced(_)) => {
+ // whenever code block or paragraph closes, new line
+ let spans = std::mem::take(&mut spans);
+ if !spans.is_empty() {
+ lines.push(Spans::from(spans));
+ }
+ lines.push(Spans::default());
}
- lines.push(Spans::default());
+ _ => (),
}
- _ => (),
}
- }
- Event::Text(text) => {
- // TODO: temp workaround
- if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(language))) = tags.last() {
- if let Some(theme) = theme {
- let rope = Rope::from(text.as_ref());
- let syntax = loader
- .language_configuration_for_injection_string(language)
- .and_then(|config| config.highlight_config(theme.scopes()))
- .map(|config| Syntax::new(&rope, config));
-
- if let Some(syntax) = syntax {
- // if we have a syntax available, highlight_iter and generate spans
- let mut highlights = Vec::new();
-
- for event in syntax.highlight_iter(rope.slice(..), None, None, |_| None)
- {
- match event.unwrap() {
- HighlightEvent::HighlightStart(span) => {
- highlights.push(span);
- }
- HighlightEvent::HighlightEnd => {
- highlights.pop();
- }
- HighlightEvent::Source { start, end } => {
- let style = match highlights.first() {
- Some(span) => theme.get(&theme.scopes()[span.0]),
- None => text_style,
- };
-
- // TODO: replace tabs with indentation
-
- let mut slice = &text[start..end];
- // TODO: do we need to handle all unicode line endings
- // here, or is just '\n' okay?
- while let Some(end) = slice.find('\n') {
- // emit span up to newline
- let text = &slice[..end];
- let text = text.replace('\t', " "); // replace tabs
- let span = Span::styled(text, style);
- spans.push(span);
-
- // truncate slice to after newline
- slice = &slice[end + 1..];
-
- // make a new line
- let spans = std::mem::take(&mut spans);
- lines.push(Spans::from(spans));
+ Event::Text(text) => {
+ // TODO: temp workaround
+ if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(language))) = tags.last() {
+ if let Some(theme) = theme {
+ let rope = Rope::from(text.as_ref());
+ let syntax = self
+ .config_loader
+ .language_configuration_for_injection_string(language)
+ .and_then(|config| config.highlight_config(theme.scopes()))
+ .map(|config| {
+ Syntax::new(&rope, config, self.config_loader.clone())
+ });
+
+ if let Some(syntax) = syntax {
+ // if we have a syntax available, highlight_iter and generate spans
+ let mut highlights = Vec::new();
+
+ for event in syntax.highlight_iter(rope.slice(..), None, None) {
+ match event.unwrap() {
+ HighlightEvent::HighlightStart(span) => {
+ highlights.push(span);
+ }
+ HighlightEvent::HighlightEnd => {
+ highlights.pop();
}
+ HighlightEvent::Source { start, end } => {
+ let style = match highlights.first() {
+ Some(span) => theme.get(&theme.scopes()[span.0]),
+ None => text_style,
+ };
- // if there's anything left, emit it too
- if !slice.is_empty() {
- let span =
- Span::styled(slice.replace('\t', " "), style);
- spans.push(span);
+ // TODO: replace tabs with indentation
+
+ let mut slice = &text[start..end];
+ // TODO: do we need to handle all unicode line endings
+ // here, or is just '\n' okay?
+ while let Some(end) = slice.find('\n') {
+ // emit span up to newline
+ let text = &slice[..end];
+ let text = text.replace('\t', " "); // replace tabs
+ let span = Span::styled(text, style);
+ spans.push(span);
+
+ // truncate slice to after newline
+ slice = &slice[end + 1..];
+
+ // make a new line
+ let spans = std::mem::take(&mut spans);
+ lines.push(Spans::from(spans));
+ }
+
+ // if there's anything left, emit it too
+ if !slice.is_empty() {
+ let span = Span::styled(
+ slice.replace('\t', " "),
+ style,
+ );
+ spans.push(span);
+ }
}
}
}
+ } else {
+ for line in text.lines() {
+ let span = Span::styled(line.to_string(), code_style);
+ lines.push(Spans::from(span));
+ }
}
} else {
for line in text.lines() {
@@ -152,64 +172,60 @@ fn parse<'a>(
lines.push(Spans::from(span));
}
}
+ } else if let Some(Tag::Heading(_, _, _)) = tags.last() {
+ let mut span = to_span(text);
+ span.style = heading_style;
+ spans.push(span);
} else {
- for line in text.lines() {
- let span = Span::styled(line.to_string(), code_style);
- lines.push(Spans::from(span));
- }
+ let mut span = to_span(text);
+ span.style = text_style;
+ spans.push(span);
}
- } else if let Some(Tag::Heading(_)) = tags.last() {
- let mut span = to_span(text);
- span.style = heading_style;
- spans.push(span);
- } else {
+ }
+ Event::Code(text) | Event::Html(text) => {
let mut span = to_span(text);
- span.style = text_style;
+ span.style = code_style;
spans.push(span);
}
+ Event::SoftBreak | Event::HardBreak => {
+ // let spans = std::mem::replace(&mut spans, Vec::new());
+ // lines.push(Spans::from(spans));
+ spans.push(Span::raw(" "));
+ }
+ Event::Rule => {
+ let mut span = Span::raw("---");
+ span.style = code_style;
+ lines.push(Spans::from(span));
+ lines.push(Spans::default());
+ }
+ // TaskListMarker(bool) true if checked
+ _ => {
+ log::warn!("unhandled markdown event {:?}", event);
+ }
}
- Event::Code(text) | Event::Html(text) => {
- let mut span = to_span(text);
- span.style = code_style;
- spans.push(span);
- }
- Event::SoftBreak | Event::HardBreak => {
- // let spans = std::mem::replace(&mut spans, Vec::new());
- // lines.push(Spans::from(spans));
- spans.push(Span::raw(" "));
- }
- Event::Rule => {
- let mut span = Span::raw("---");
- span.style = code_style;
- lines.push(Spans::from(span));
- lines.push(Spans::default());
- }
- // TaskListMarker(bool) true if checked
- _ => {
- log::warn!("unhandled markdown event {:?}", event);
- }
+ // build up a vec of Paragraph tui widgets
}
- // build up a vec of Paragraph tui widgets
- }
- if !spans.is_empty() {
- lines.push(Spans::from(spans));
- }
+ if !spans.is_empty() {
+ lines.push(Spans::from(spans));
+ }
- // if last line is empty, remove it
- if let Some(line) = lines.last() {
- if line.0.is_empty() {
- lines.pop();
+ // if last line is empty, remove it
+ if let Some(line) = lines.last() {
+ if line.0.is_empty() {
+ lines.pop();
+ }
}
- }
- Text::from(lines)
+ Text::from(lines)
+ }
}
+
impl Component for Markdown {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
use tui::widgets::{Paragraph, Widget, Wrap};
- let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
+ let text = self.parse(Some(&cx.editor.theme));
let par = Paragraph::new(text)
.wrap(Wrap { trim: false })
@@ -227,7 +243,8 @@ impl Component for Markdown {
if padding >= viewport.1 || padding >= viewport.0 {
return None;
}
- let contents = parse(&self.contents, None, &self.config_loader);
+ let contents = self.parse(None);
+
// TODO: account for tab width
let max_text_width = (viewport.0 - padding).min(120);
let mut text_width = 0;
@@ -241,11 +258,6 @@ impl Component for Markdown {
} else if content_width > text_width {
text_width = content_width;
}
-
- if height >= viewport.1 {
- height = viewport.1;
- break;
- }
}
Some((text_width + padding, height))
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index e891c149..f9a0438c 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -14,11 +14,18 @@ use helix_view::{graphics::Rect, Editor};
use tui::layout::Constraint;
pub trait Item {
- fn sort_text(&self) -> &str;
- fn filter_text(&self) -> &str;
-
fn label(&self) -> &str;
- fn row(&self) -> Row;
+
+ fn sort_text(&self) -> &str {
+ self.label()
+ }
+ fn filter_text(&self) -> &str {
+ self.label()
+ }
+
+ fn row(&self) -> Row {
+ Row::new(vec![Cell::from(self.label())])
+ }
}
pub struct Menu<T: Item> {
@@ -132,7 +139,17 @@ impl<T: Item> Menu<T> {
acc
});
- let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar
+
+ let height = self.matches.len().min(10).min(viewport.1 as usize);
+ // do all the matches fit on a single screen?
+ let fits = self.matches.len() <= height;
+
+ let mut len = max_lens.iter().sum::<usize>() + n;
+
+ if !fits {
+ len += 1; // +1: reserve some space for scrollbar
+ }
+
let width = len.min(viewport.0 as usize);
self.widths = max_lens
@@ -140,8 +157,6 @@ impl<T: Item> Menu<T> {
.map(|len| Constraint::Length(len as u16))
.collect();
- let height = self.matches.len().min(10).min(viewport.1 as usize);
-
self.size = (width as u16, height as u16);
// adjust scroll offsets if size changed
@@ -190,7 +205,7 @@ impl<T: Item + 'static> Component for Menu<T> {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@@ -202,7 +217,7 @@ impl<T: Item + 'static> Component for Menu<T> {
return close_fn;
}
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
- shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
+ shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
return EventResult::Consumed(None);
@@ -297,12 +312,14 @@ impl<T: Item + 'static> Component for Menu<T> {
},
);
+ let fits = len <= win_height;
+
for (i, _) in (scroll..(scroll + win_height).min(len)).enumerate() {
let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
- if is_marked {
- let cell = surface.get_mut(area.x + area.width - 2, area.y + i as u16);
- cell.set_symbol("▐ ");
+ if !fits && is_marked {
+ let cell = &mut surface[(area.x + area.width - 2, area.y + i as u16)];
+ cell.set_symbol("▐");
// cell.set_style(selected);
// cell.set_style(if is_marked { selected } else { style });
}
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 3c203326..49f7b2fa 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -2,7 +2,7 @@ mod completion;
pub(crate) mod editor;
mod info;
mod markdown;
-mod menu;
+pub mod menu;
mod picker;
mod popup;
mod prompt;
@@ -65,7 +65,7 @@ pub fn regex_prompt(
return;
}
- let case_insensitive = if cx.editor.config.smart_case {
+ let case_insensitive = if cx.editor.config.search.smart_case {
!input.chars().any(char::is_uppercase)
} else {
false
@@ -174,7 +174,9 @@ pub mod completers {
use crate::ui::prompt::Completion;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
+ use helix_view::editor::Config;
use helix_view::theme;
+ use once_cell::sync::Lazy;
use std::borrow::Cow;
use std::cmp::Reverse;
@@ -186,6 +188,7 @@ pub mod completers {
&helix_core::config_dir().join("themes"),
));
names.push("default".into());
+ names.push("base16_default".into());
let mut names: Vec<_> = names
.into_iter()
@@ -207,6 +210,31 @@ pub mod completers {
names
}
+ pub fn setting(input: &str) -> Vec<Completion> {
+ static KEYS: Lazy<Vec<String>> = Lazy::new(|| {
+ serde_json::to_value(Config::default())
+ .unwrap()
+ .as_object()
+ .unwrap()
+ .keys()
+ .cloned()
+ .collect()
+ });
+
+ let matcher = Matcher::default();
+
+ let mut matches: Vec<_> = KEYS
+ .iter()
+ .filter_map(|name| matcher.fuzzy_match(name, input).map(|score| (name, score)))
+ .collect();
+
+ matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
+ matches
+ .into_iter()
+ .map(|(name, _)| ((0..), name.into()))
+ .collect()
+ }
+
pub fn filename(input: &str) -> Vec<Completion> {
filename_impl(input, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
@@ -255,7 +283,7 @@ pub mod completers {
let is_tilde = input.starts_with('~') && input.len() == 1;
let path = helix_core::path::expand_tilde(Path::new(input));
- let (dir, file_name) = if input.ends_with('/') {
+ let (dir, file_name) = if input.ends_with(std::path::MAIN_SEPARATOR) {
(path, None)
} else {
let file_name = path
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index eaca470e..2c7db7f2 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -46,7 +46,7 @@ pub struct FilePicker<T> {
}
pub enum CachedPreview {
- Document(Document),
+ Document(Box<Document>),
Binary,
LargeFile,
NotFound,
@@ -139,8 +139,8 @@ impl<T> FilePicker<T> {
(size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile,
_ => {
// TODO: enable syntax highlighting; blocked by async rendering
- Document::open(path, None, Some(&editor.theme), None)
- .map(CachedPreview::Document)
+ Document::open(path, None, None)
+ .map(|doc| CachedPreview::Document(Box::new(doc)))
.unwrap_or(CachedPreview::NotFound)
}
},
@@ -159,6 +159,7 @@ impl<T: 'static> Component for FilePicker<T> {
// |picker | | |
// | | | |
// +---------+ +---------+
+
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
let area = inner_rect(area);
// -- Render the frame:
@@ -220,13 +221,8 @@ impl<T: 'static> Component for FilePicker<T> {
let offset = Position::new(first_line, 0);
- let highlights = EditorView::doc_syntax_highlights(
- doc,
- offset,
- area.height,
- &cx.editor.theme,
- &cx.editor.syn_loader,
- );
+ let highlights =
+ EditorView::doc_syntax_highlights(doc, offset, area.height, &cx.editor.theme);
EditorView::render_text_highlights(
doc,
offset,
@@ -397,6 +393,16 @@ fn inner_rect(area: Rect) -> Rect {
}
impl<T: 'static> Component for Picker<T> {
+ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
+ let max_width = 50.min(viewport.0);
+ let max_height = 10.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
+
+ let height = (self.options.len() as u16 + 4) // add some spacing for input + padding
+ .min(max_height);
+ let width = max_width;
+ Some((width, height))
+ }
+
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let key_event = match event {
Event::Key(event) => event,
@@ -404,13 +410,13 @@ impl<T: 'static> Component for Picker<T> {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.last_picker = compositor.pop();
})));
match key_event.into() {
- shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
+ shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up();
}
key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
@@ -492,10 +498,9 @@ impl<T: 'static> Component for Picker<T> {
let sep_style = Style::default().fg(Color::Rgb(90, 89, 119));
let borders = BorderType::line_symbols(BorderType::Plain);
for x in inner.left()..inner.right() {
- surface
- .get_mut(x, inner.y + 1)
- .set_symbol(borders.horizontal)
- .set_style(sep_style);
+ if let Some(cell) = surface.get_mut(x, inner.y + 1) {
+ cell.set_symbol(borders.horizontal).set_style(sep_style);
+ }
}
// -- Render the contents:
@@ -505,7 +510,7 @@ impl<T: 'static> Component for Picker<T> {
let selected = cx.editor.theme.get("ui.text.focus");
let rows = inner.height;
- let offset = self.cursor / (rows as usize) * (rows as usize);
+ let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
let files = self.matches.iter().skip(offset).map(|(index, _score)| {
(index, self.options.get(*index).unwrap()) // get_unchecked
@@ -513,7 +518,7 @@ impl<T: 'static> Component for Picker<T> {
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
if i == (self.cursor - offset) {
- surface.set_string(inner.x - 2, inner.y + i as u16, ">", selected);
+ surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected);
}
surface.set_string_truncated(
diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs
index 8f7921a1..4d319423 100644
--- a/helix-term/src/ui/popup.rs
+++ b/helix-term/src/ui/popup.rs
@@ -6,7 +6,7 @@ use crossterm::event::Event;
use tui::buffer::Buffer as Surface;
use helix_core::Position;
-use helix_view::graphics::Rect;
+use helix_view::graphics::{Margin, Rect};
// TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return
// a width/height hint. maybe Popup(Box<Component>)
@@ -14,17 +14,26 @@ use helix_view::graphics::Rect;
pub struct Popup<T: Component> {
contents: T,
position: Option<Position>,
+ margin: Margin,
size: (u16, u16),
+ child_size: (u16, u16),
scroll: usize,
+ id: &'static str,
}
impl<T: Component> Popup<T> {
- pub fn new(contents: T) -> Self {
+ pub fn new(id: &'static str, contents: T) -> Self {
Self {
contents,
position: None,
+ margin: Margin {
+ vertical: 0,
+ horizontal: 0,
+ },
size: (0, 0),
+ child_size: (0, 0),
scroll: 0,
+ id,
}
}
@@ -32,6 +41,11 @@ impl<T: Component> Popup<T> {
self.position = pos;
}
+ pub fn margin(mut self, margin: Margin) -> Self {
+ self.margin = margin;
+ self
+ }
+
pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
let position = self
.position
@@ -68,6 +82,9 @@ impl<T: Component> Popup<T> {
pub fn scroll(&mut self, offset: usize, direction: bool) {
if direction {
self.scroll += offset;
+
+ let max_offset = self.child_size.1.saturating_sub(self.size.1);
+ self.scroll = (self.scroll + offset).min(max_offset as usize);
} else {
self.scroll = self.scroll.saturating_sub(offset);
}
@@ -93,7 +110,7 @@ impl<T: Component> Component for Popup<T> {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@@ -115,13 +132,26 @@ impl<T: Component> Component for Popup<T> {
// tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
}
- fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
+ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
+ let max_width = 120.min(viewport.0);
+ let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
+
+ let inner = Rect::new(0, 0, max_width, max_height).inner(&self.margin);
+
let (width, height) = self
.contents
- .required_size((120, 26)) // max width, max height
+ .required_size((inner.width, inner.height))
.expect("Component needs required_size implemented in order to be embedded in a popup");
- self.size = (width, height);
+ self.child_size = (width, height);
+ self.size = (
+ (width + self.margin.horizontal * 2).min(max_width),
+ (height + self.margin.vertical * 2).min(max_height),
+ );
+
+ // re-clamp scroll offset
+ let max_offset = self.child_size.1.saturating_sub(self.size.1);
+ self.scroll = self.scroll.min(max_offset as usize);
Some(self.size)
}
@@ -141,6 +171,11 @@ impl<T: Component> Component for Popup<T> {
let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background);
- self.contents.render(area, surface, cx);
+ let inner = area.inner(&self.margin);
+ self.contents.render(inner, surface, cx);
+ }
+
+ fn id(&self) -> Option<&'static str> {
+ Some(self.id)
}
}
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index e90b0772..4c4fef26 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -127,7 +127,7 @@ impl Prompt {
let mut char_position = char_indices
.iter()
.position(|(idx, _)| *idx == self.cursor)
- .unwrap_or_else(|| char_indices.len());
+ .unwrap_or(char_indices.len());
for _ in 0..rep {
// Skip any non-whitespace characters
@@ -330,7 +330,7 @@ impl Prompt {
.max(BASE_WIDTH);
let cols = std::cmp::max(1, area.width / max_len);
- let col_width = (area.width - (cols)) / cols;
+ let col_width = (area.width.saturating_sub(cols)) / cols;
let height = ((self.completion.len() as u16 + cols - 1) / cols)
.min(10) // at most 10 rows (or less)
@@ -426,7 +426,7 @@ impl Component for Prompt {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@@ -473,7 +473,7 @@ impl Component for Prompt {
}
}
key!(Enter) => {
- if self.selection.is_some() && self.line.ends_with('/') {
+ if self.selection.is_some() && self.line.ends_with(std::path::MAIN_SEPARATOR) {
self.completion = (self.completion_fn)(&self.line);
self.exit_selection();
} else {
@@ -505,7 +505,7 @@ impl Component for Prompt {
self.change_completion_selection(CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
- shift!(BackTab) => {
+ shift!(Tab) => {
self.change_completion_selection(CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
diff --git a/helix-term/src/ui/spinner.rs b/helix-term/src/ui/spinner.rs
index e8a43b48..68965469 100644
--- a/helix-term/src/ui/spinner.rs
+++ b/helix-term/src/ui/spinner.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashMap, time::SystemTime};
+use std::{collections::HashMap, time::Instant};
#[derive(Default, Debug)]
pub struct ProgressSpinners {
@@ -25,7 +25,7 @@ impl Default for Spinner {
pub struct Spinner {
frames: Vec<&'static str>,
count: usize,
- start: Option<SystemTime>,
+ start: Option<Instant>,
interval: u64,
}
@@ -50,14 +50,13 @@ impl Spinner {
}
pub fn start(&mut self) {
- self.start = Some(SystemTime::now());
+ self.start = Some(Instant::now());
}
pub fn frame(&self) -> Option<&str> {
let idx = (self
.start
- .map(|time| SystemTime::now().duration_since(time))?
- .ok()?
+ .map(|time| Instant::now().duration_since(time))?
.as_millis()
/ self.interval as u128) as usize
% self.count;