summaryrefslogtreecommitdiff
path: root/helix-term
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term')
-rw-r--r--helix-term/build.rs12
-rw-r--r--helix-term/src/application.rs64
-rw-r--r--helix-term/src/commands.rs829
-rw-r--r--helix-term/src/compositor.rs5
-rw-r--r--helix-term/src/config.rs3
-rw-r--r--helix-term/src/keymap.rs171
-rw-r--r--helix-term/src/main.rs34
-rw-r--r--helix-term/src/ui/editor.rs41
-rw-r--r--helix-term/src/ui/markdown.rs14
-rw-r--r--helix-term/src/ui/menu.rs57
-rw-r--r--helix-term/src/ui/mod.rs2
-rw-r--r--helix-term/src/ui/picker.rs73
-rw-r--r--helix-term/src/ui/popup.rs28
-rw-r--r--helix-term/src/ui/prompt.rs158
14 files changed, 1053 insertions, 438 deletions
diff --git a/helix-term/build.rs b/helix-term/build.rs
new file mode 100644
index 00000000..61ffa6f4
--- /dev/null
+++ b/helix-term/build.rs
@@ -0,0 +1,12 @@
+use std::process::Command;
+
+fn main() {
+ let git_hash = Command::new("git")
+ .args(&["describe", "--dirty"])
+ .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);
+}
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index faf93b09..242dc837 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -9,7 +9,7 @@ use crate::{
use log::{error, warn};
use std::{
- io::{stdout, Write},
+ io::{stdin, stdout, Write},
sync::Arc,
time::{Duration, Instant},
};
@@ -19,6 +19,7 @@ use anyhow::Error;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
execute, terminal,
+ tty::IsTty,
};
#[cfg(not(windows))]
use {
@@ -62,14 +63,19 @@ impl Application {
std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
// load default and user config, and merge both
- let def_lang_conf: toml::Value = toml::from_slice(include_bytes!("../../languages.toml"))
- .expect("Could not parse built-in languages.toml, something must be very wrong");
- let user_lang_conf: Option<toml::Value> = std::fs::read(conf_dir.join("languages.toml"))
+ let builtin_err_msg =
+ "Could not parse built-in languages.toml, something must be very wrong";
+ let def_lang_conf: toml::Value =
+ toml::from_slice(include_bytes!("../../languages.toml")).expect(builtin_err_msg);
+ let def_syn_loader_conf: helix_core::syntax::Configuration =
+ def_lang_conf.clone().try_into().expect(builtin_err_msg);
+ let user_lang_conf = std::fs::read(conf_dir.join("languages.toml"))
.ok()
- .map(|raw| toml::from_slice(&raw).expect("Could not parse user languages.toml"));
+ .map(|raw| toml::from_slice(&raw));
let lang_conf = match user_lang_conf {
- Some(value) => merge_toml_values(def_lang_conf, value),
- None => def_lang_conf,
+ Some(Ok(value)) => Ok(merge_toml_values(def_lang_conf, value)),
+ Some(err @ Err(_)) => err,
+ None => Ok(def_lang_conf),
};
let theme = if let Some(theme) = &config.theme {
@@ -85,8 +91,15 @@ impl Application {
};
let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
- .try_into()
- .expect("Could not parse merged (built-in + user) languages.toml");
+ .and_then(|conf| conf.try_into())
+ .unwrap_or_else(|err| {
+ eprintln!("Bad language config: {}", err);
+ eprintln!("Press <ENTER> to continue with default language config");
+ use std::io::Read;
+ // This waits for an enter press.
+ let _ = std::io::stdin().read(&mut []);
+ def_syn_loader_conf
+ });
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let mut editor = Editor::new(
@@ -124,8 +137,17 @@ impl Application {
}
editor.set_status(format!("Loaded {} files.", nr_of_files));
}
- } else {
+ } else if stdin().is_tty() {
editor.new_file(Action::VerticalSplit);
+ } else if cfg!(target_os = "macos") {
+ // On Linux and Windows, we allow the output of a command to be piped into the new buffer.
+ // This doesn't currently work on macOS because of the following issue:
+ // https://github.com/crossterm-rs/crossterm/issues/500
+ anyhow::bail!("Piping into helix-term is currently not supported on macOS");
+ } else {
+ editor
+ .new_file_from_stdin(Action::VerticalSplit)
+ .unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
}
editor.set_theme(theme);
@@ -253,12 +275,8 @@ impl Application {
}
let editor_view = self
.compositor
- .find(std::any::type_name::<ui::EditorView>())
+ .find::<ui::EditorView>()
.expect("expected at least one EditorView");
- let editor_view = editor_view
- .as_any_mut()
- .downcast_mut::<ui::EditorView>()
- .unwrap();
if editor_view.completion.is_some() {
return;
@@ -583,12 +601,8 @@ impl Application {
{
let editor_view = self
.compositor
- .find(std::any::type_name::<ui::EditorView>())
+ .find::<ui::EditorView>()
.expect("expected at least one EditorView");
- let editor_view = editor_view
- .as_any_mut()
- .downcast_mut::<ui::EditorView>()
- .unwrap();
let lsp::ProgressParams { token, value } = params;
let lsp::ProgressParamsValue::WorkDone(work) = value;
@@ -702,12 +716,8 @@ impl Application {
let editor_view = self
.compositor
- .find(std::any::type_name::<ui::EditorView>())
+ .find::<ui::EditorView>()
.expect("expected at least one EditorView");
- let editor_view = editor_view
- .as_any_mut()
- .downcast_mut::<ui::EditorView>()
- .unwrap();
let spinner = editor_view.spinners_mut().get_or_create(server_id);
if spinner.is_stopped() {
spinner.start();
@@ -742,7 +752,7 @@ impl Application {
Ok(())
}
- pub async fn run(&mut self) -> Result<(), Error> {
+ pub async fn run(&mut self) -> Result<i32, Error> {
self.claim_term().await?;
// Exit the alternate screen and disable raw mode before panicking
@@ -765,6 +775,6 @@ impl Application {
self.restore_term()?;
- Ok(())
+ Ok(self.editor.exit_code)
}
}
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 12dd2460..56b31b67 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -3,21 +3,25 @@ pub(crate) mod dap;
pub use dap::*;
use helix_core::{
- comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent,
+ comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
+ history::UndoKind,
+ indent,
indent::IndentStyle,
line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
match_brackets,
movement::{self, Direction},
+ numbers::NumberIncrementor,
object, pos_at_coords,
regex::{self, Regex, RegexBuilder},
register::Register,
- search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes,
- RopeSlice, Selection, SmallVec, Tendril, Transaction,
+ search, selection, surround, textobject,
+ unicode::width::UnicodeWidthChar,
+ LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
+ Transaction,
};
-
use helix_view::{
clipboard::ClipboardType,
- document::Mode,
+ document::{Mode, SCRATCH_BUFFER_NAME},
editor::{Action, Motion},
input::KeyEvent,
keyboard::KeyCode,
@@ -27,7 +31,7 @@ use helix_view::{
use anyhow::{anyhow, bail, Context as _};
use helix_lsp::{
- lsp,
+ block_on, lsp,
util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
OffsetEncoding,
};
@@ -189,6 +193,7 @@ impl Command {
copy_selection_on_prev_line, "Copy selection on previous line",
move_next_word_start, "Move to beginning of next word",
move_prev_word_start, "Move to beginning of previous word",
+ move_prev_word_end, "Move to end of previous word",
move_next_word_end, "Move to end of next word",
move_next_long_word_start, "Move to beginning of next long word",
move_prev_long_word_start, "Move to beginning of previous long word",
@@ -241,6 +246,7 @@ impl Command {
code_action, "Perform code action",
buffer_picker, "Open buffer picker",
symbol_picker, "Open symbol picker",
+ workspace_symbol_picker, "Open workspace symbol picker",
last_picker, "Open last picker",
prepend_to_line, "Insert at start of line",
append_to_line, "Insert at end of line",
@@ -261,6 +267,7 @@ impl Command {
goto_window_middle, "Goto window middle",
goto_window_bottom, "Goto window bottom",
goto_last_accessed_file, "Goto last accessed file",
+ goto_last_modification, "Goto last modification",
goto_line, "Goto line",
goto_last_line, "Goto last line",
goto_first_diag, "Goto first diagnostic",
@@ -274,6 +281,7 @@ impl Command {
// TODO: different description ?
goto_line_end_newline, "Goto line end",
goto_first_nonwhitespace, "Goto first non-blank in line",
+ trim_selections, "Trim whitespace from selections",
extend_to_line_start, "Extend to line start",
extend_to_line_end, "Extend to line end",
extend_to_line_end_newline, "Extend to line end",
@@ -283,8 +291,13 @@ impl Command {
delete_char_backward, "Delete previous char",
delete_char_forward, "Delete next char",
delete_word_backward, "Delete previous word",
+ delete_word_forward, "Delete next word",
+ kill_to_line_start, "Delete content till the start of the line",
+ kill_to_line_end, "Delete content till the end of the line",
undo, "Undo change",
redo, "Redo change",
+ earlier, "Move backward in history",
+ later, "Move forward in history",
yank, "Yank selection",
yank_joined_to_clipboard, "Join and yank selections to clipboard",
yank_main_selection_to_clipboard, "Yank main selection to clipboard",
@@ -304,6 +317,7 @@ impl Command {
format_selections, "Format selection",
join_selections, "Join lines inside selection",
keep_selections, "Keep selections matching regex",
+ remove_selections, "Remove selections matching regex",
keep_primary_selection, "Keep primary selection",
remove_primary_selection, "Remove primary selection",
completion, "Invoke completion popup",
@@ -324,7 +338,9 @@ impl Command {
hsplit, "Horizontal bottom split",
vsplit, "Vertical right split",
wclose, "Close window",
+ wonly, "Current window only",
select_register, "Select register",
+ insert_register, "Insert register",
align_view_middle, "Align view middle",
align_view_top, "Align view top",
align_view_center, "Align view center",
@@ -358,6 +374,9 @@ impl Command {
shell_append_output, "Append output of shell command after each selection",
shell_keep_pipe, "Filter selections with shell predicate",
suspend, "Suspend",
+ rename_symbol, "Rename symbol",
+ increment, "Increment",
+ decrement, "Decrement",
);
}
@@ -581,6 +600,29 @@ fn extend_to_line_start(cx: &mut Context) {
goto_line_start_impl(view, doc, Movement::Extend)
}
+fn kill_to_line_start(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+
+ let selection = doc.selection(view.id).clone().transform(|range| {
+ let line = range.cursor_line(text);
+ range.put_cursor(text, text.line_to_char(line), true)
+ });
+ delete_selection_insert_mode(doc, view, &selection);
+}
+
+fn kill_to_line_end(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+
+ 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)
+ });
+ delete_selection_insert_mode(doc, view, &selection);
+}
+
fn goto_first_nonwhitespace(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
@@ -598,6 +640,42 @@ fn goto_first_nonwhitespace(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
+fn trim_selections(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+
+ let ranges: SmallVec<[Range; 1]> = doc
+ .selection(view.id)
+ .iter()
+ .filter_map(|range| {
+ if range.is_empty() || range.fragment(text).chars().all(|ch| ch.is_whitespace()) {
+ return None;
+ }
+ let mut start = range.from();
+ let mut end = range.to();
+ start = movement::skip_while(text, start, |x| x.is_whitespace()).unwrap_or(start);
+ end = movement::backwards_skip_while(text, end, |x| x.is_whitespace()).unwrap_or(end);
+ if range.anchor < range.head {
+ Some(Range::new(start, end))
+ } else {
+ Some(Range::new(end, start))
+ }
+ })
+ .collect();
+
+ if !ranges.is_empty() {
+ let primary = doc.selection(view.id).primary();
+ let idx = ranges
+ .iter()
+ .position(|range| range.overlaps(&primary))
+ .unwrap_or(ranges.len() - 1);
+ doc.set_selection(view.id, Selection::new(ranges, idx));
+ } else {
+ collapse_selection(cx);
+ keep_primary_selection(cx);
+ };
+}
+
fn goto_window(cx: &mut Context, align: Align) {
let (view, doc) = current!(cx.editor);
@@ -657,6 +735,10 @@ fn move_prev_word_start(cx: &mut Context) {
move_word_impl(cx, movement::move_prev_word_start)
}
+fn move_prev_word_end(cx: &mut Context) {
+ move_word_impl(cx, movement::move_prev_word_end)
+}
+
fn move_next_word_end(cx: &mut Context) {
move_word_impl(cx, movement::move_next_word_end)
}
@@ -1193,6 +1275,7 @@ fn search_impl(
regex: &Regex,
movement: Movement,
direction: Direction,
+ scrolloff: usize,
) {
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
@@ -1251,7 +1334,11 @@ fn search_impl(
};
doc.set_selection(view.id, selection);
- align_view(doc, view, Align::Center);
+ if view.is_cursor_in_view(doc, 0) {
+ view.ensure_cursor_in_view(doc, scrolloff);
+ } else {
+ align_view(doc, view, Align::Center)
+ }
};
}
@@ -1275,6 +1362,8 @@ fn rsearch(cx: &mut Context) {
// TODO: use one function for search vs extend
fn searcher(cx: &mut Context, direction: Direction) {
let reg = cx.register.unwrap_or('/');
+ let scrolloff = cx.editor.config.scrolloff;
+
let (_, doc) = current!(cx.editor);
// TODO: could probably share with select_on_matches?
@@ -1299,7 +1388,15 @@ fn searcher(cx: &mut Context, direction: Direction) {
if event != PromptEvent::Update {
return;
}
- search_impl(doc, view, &contents, &regex, Movement::Move, direction);
+ search_impl(
+ doc,
+ view,
+ &contents,
+ &regex,
+ Movement::Move,
+ direction,
+ scrolloff,
+ );
},
);
@@ -1307,6 +1404,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
}
fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
+ let scrolloff = cx.editor.config.scrolloff;
let (view, doc) = current!(cx.editor);
let registers = &cx.editor.registers;
if let Some(query) = registers.read('/') {
@@ -1321,7 +1419,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
.case_insensitive(case_insensitive)
.build()
{
- search_impl(doc, view, &contents, &regex, movement, direction);
+ search_impl(doc, view, &contents, &regex, movement, direction, scrolloff);
} else {
// get around warning `mutable_borrow_reservation_conflict`
// which will be a hard error in the future
@@ -1365,7 +1463,7 @@ fn global_search(cx: &mut Context) {
let completions = search_completions(cx, None);
let prompt = ui::regex_prompt(
cx,
- "global search:".into(),
+ "global-search:".into(),
None,
move |input: &str| {
completions
@@ -1543,6 +1641,17 @@ fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId
doc.apply(&transaction, view_id);
}
+#[inline]
+fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Selection) {
+ let view_id = view.id;
+
+ // then delete
+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
+ (range.from(), range.to(), None)
+ });
+ doc.apply(&transaction, view_id);
+}
+
fn delete_selection(cx: &mut Context) {
let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
@@ -1657,8 +1766,7 @@ mod cmd {
buffers_remaining_impl(cx.editor)?
}
- cx.editor
- .close(view!(cx.editor).id, /* close_buffer */ false);
+ cx.editor.close(view!(cx.editor).id);
Ok(())
}
@@ -1668,8 +1776,7 @@ mod cmd {
_args: &[&str],
_event: PromptEvent,
) -> anyhow::Result<()> {
- cx.editor
- .close(view!(cx.editor).id, /* close_buffer */ false);
+ cx.editor.close(view!(cx.editor).id);
Ok(())
}
@@ -1679,11 +1786,30 @@ mod cmd {
args: &[&str],
_event: PromptEvent,
) -> anyhow::Result<()> {
- use helix_core::path::expand_tilde;
let path = args.get(0).context("wrong argument count")?;
- let _ = cx
- .editor
- .open(expand_tilde(Path::new(path)), Action::Replace)?;
+ let _ = cx.editor.open(path.into(), Action::Replace)?;
+ Ok(())
+ }
+
+ fn buffer_close(
+ cx: &mut compositor::Context,
+ _args: &[&str],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let view = view!(cx.editor);
+ let doc_id = view.doc;
+ cx.editor.close_document(doc_id, false)?;
+ Ok(())
+ }
+
+ fn force_buffer_close(
+ cx: &mut compositor::Context,
+ _args: &[&str],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let view = view!(cx.editor);
+ let doc_id = view.doc;
+ cx.editor.close_document(doc_id, true)?;
Ok(())
}
@@ -1838,13 +1964,13 @@ mod cmd {
args: &[&str],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let uk = args
- .join(" ")
- .parse::<helix_core::history::UndoKind>()
- .map_err(|s| anyhow!(s))?;
+ let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
let (view, doc) = current!(cx.editor);
- doc.earlier(view.id, uk);
+ let success = doc.earlier(view.id, uk);
+ if !success {
+ cx.editor.set_status("Already at oldest change".to_owned());
+ }
Ok(())
}
@@ -1854,12 +1980,12 @@ mod cmd {
args: &[&str],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let uk = args
- .join(" ")
- .parse::<helix_core::history::UndoKind>()
- .map_err(|s| anyhow!(s))?;
+ let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
let (view, doc) = current!(cx.editor);
- doc.later(view.id, uk);
+ let success = doc.later(view.id, uk);
+ if !success {
+ cx.editor.set_status("Already at newest change".to_owned());
+ }
Ok(())
}
@@ -1891,7 +2017,7 @@ mod cmd {
.map(|doc| {
doc.relative_path()
.map(|path| path.to_string_lossy().to_string())
- .unwrap_or_else(|| "[scratch]".into())
+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
})
.collect();
if !modified.is_empty() {
@@ -1933,7 +2059,7 @@ mod cmd {
// close all views
let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
for view_id in views {
- cx.editor.close(view_id, false);
+ cx.editor.close(view_id);
}
}
@@ -1977,7 +2103,7 @@ mod cmd {
// close all views
let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
for view_id in views {
- editor.close(view_id, false);
+ editor.close(view_id);
}
Ok(())
@@ -1999,6 +2125,25 @@ mod cmd {
quit_all_impl(&mut cx.editor, args, event, true)
}
+ fn cquit(
+ cx: &mut compositor::Context,
+ args: &[&str],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let exit_code = args
+ .first()
+ .and_then(|code| code.parse::<i32>().ok())
+ .unwrap_or(1);
+ cx.editor.exit_code = exit_code;
+
+ let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
+ for view_id in views {
+ cx.editor.close(view_id);
+ }
+
+ Ok(())
+ }
+
fn theme(
cx: &mut compositor::Context,
args: &[&str],
@@ -2241,7 +2386,6 @@ mod cmd {
args: &[&str],
_event: PromptEvent,
) -> anyhow::Result<()> {
- use helix_lsp::block_on;
if let Some(debugger) = cx.editor.debugger.as_mut() {
let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) {
(Some(frame), Some(thread_id)) => (frame, thread_id),
@@ -2345,6 +2489,20 @@ 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
+ },
+ TypableCommand {
+ name: "buffer-close!",
+ aliases: &["bc!", "bclose!"],
+ doc: "Close the current buffer forcefully (ignoring unsaved changes).",
+ fun: force_buffer_close,
+ completer: None, // FIXME: buffer completer
+ },
+ TypableCommand {
name: "write",
aliases: &["w"],
doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
@@ -2443,6 +2601,13 @@ mod cmd {
completer: None,
},
TypableCommand {
+ name: "cquit",
+ aliases: &["cq"],
+ doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
+ fun: cquit,
+ completer: None,
+ },
+ TypableCommand {
name: "theme",
aliases: &[],
doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)",
@@ -2698,36 +2863,66 @@ fn file_picker(cx: &mut Context) {
fn buffer_picker(cx: &mut Context) {
let current = view!(cx.editor).doc;
+ struct BufferMeta {
+ id: DocumentId,
+ path: Option<PathBuf>,
+ is_modified: bool,
+ is_current: bool,
+ }
+
+ impl BufferMeta {
+ fn format(&self) -> Cow<str> {
+ let path = self
+ .path
+ .as_deref()
+ .map(helix_core::path::get_relative_path);
+ let path = match path.as_deref().and_then(Path::to_str) {
+ Some(path) => path,
+ None => return Cow::Borrowed(SCRATCH_BUFFER_NAME),
+ };
+
+ let mut flags = Vec::new();
+ if self.is_modified {
+ flags.push("+");
+ }
+ if self.is_current {
+ flags.push("*");
+ }
+
+ let flag = if flags.is_empty() {
+ "".into()
+ } else {
+ format!(" ({})", flags.join(""))
+ };
+ Cow::Owned(format!("{}{}", path, flag))
+ }
+ }
+
+ let new_meta = |doc: &Document| BufferMeta {
+ id: doc.id(),
+ path: doc.path().cloned(),
+ is_modified: doc.is_modified(),
+ is_current: doc.id() == current,
+ };
+
let picker = FilePicker::new(
cx.editor
.documents
.iter()
- .map(|(id, doc)| (*id, doc.path().cloned()))
+ .map(|(_, doc)| new_meta(doc))
.collect(),
- move |(id, path): &(DocumentId, Option<PathBuf>)| {
- let path = path.as_deref().map(helix_core::path::get_relative_path);
- match path.as_ref().and_then(|path| path.to_str()) {
- Some(path) => {
- if *id == current {
- format!("{} (*)", &path).into()
- } else {
- path.to_owned().into()
- }
- }
- None => "[scratch buffer]".into(),
- }
- },
- |cx, (id, _path): &(DocumentId, Option<PathBuf>), _action| {
- cx.editor.switch(*id, Action::Replace);
+ BufferMeta::format,
+ |cx, meta, _action| {
+ cx.editor.switch(meta.id, Action::Replace);
},
- |editor, (id, path)| {
- let doc = &editor.documents.get(id)?;
+ |editor, meta| {
+ let doc = &editor.documents.get(&meta.id)?;
let &view_id = doc.selections().keys().next()?;
let line = doc
.selection(view_id)
.primary()
.cursor_line(doc.text().slice(..));
- Some((path.clone()?, Some((line, line))))
+ Some((meta.path.clone()?, Some((line, line))))
},
);
cx.push_layer(Box::new(picker));
@@ -2782,7 +2977,7 @@ fn symbol_picker(cx: &mut Context) {
}
};
- let picker = FilePicker::new(
+ let mut picker = FilePicker::new(
symbols,
|symbol| (&symbol.name).into(),
move |cx, symbol, _action| {
@@ -2807,6 +3002,69 @@ fn symbol_picker(cx: &mut Context) {
Some((path, line))
},
);
+ picker.truncate_start = false;
+ compositor.push(Box::new(picker))
+ }
+ },
+ )
+}
+
+fn workspace_symbol_picker(cx: &mut Context) {
+ let (_, doc) = current!(cx.editor);
+
+ let language_server = match doc.language_server() {
+ Some(language_server) => language_server,
+ None => return,
+ };
+ let offset_encoding = language_server.offset_encoding();
+
+ let future = language_server.workspace_symbols("".to_string());
+
+ let current_path = doc_mut!(cx.editor).path().cloned();
+ cx.callback(
+ future,
+ move |_editor: &mut Editor,
+ compositor: &mut Compositor,
+ response: Option<Vec<lsp::SymbolInformation>>| {
+ if let Some(symbols) = response {
+ let mut picker = FilePicker::new(
+ symbols,
+ move |symbol| {
+ let path = symbol.location.uri.to_file_path().unwrap();
+ if current_path.as_ref().map(|p| p == &path).unwrap_or(false) {
+ (&symbol.name).into()
+ } else {
+ let relative_path = helix_core::path::get_relative_path(path.as_path())
+ .to_str()
+ .unwrap()
+ .to_owned();
+ format!("{} ({})", &symbol.name, relative_path).into()
+ }
+ },
+ move |cx, symbol, action| {
+ let path = symbol.location.uri.to_file_path().unwrap();
+ cx.editor.open(path, action).expect("editor.open failed");
+ let (view, doc) = current!(cx.editor);
+
+ if let Some(range) =
+ lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
+ {
+ // we flip the range so that the cursor sits on the start of the symbol
+ // (for example start of the function).
+ doc.set_selection(view.id, Selection::single(range.head, range.anchor));
+ align_view(doc, view, Align::Center);
+ }
+ },
+ move |_editor, symbol| {
+ let path = symbol.location.uri.to_file_path().unwrap();
+ let line = Some((
+ symbol.location.range.start.line as usize,
+ symbol.location.range.end.line as usize,
+ ));
+ Some((path, line))
+ },
+ );
+ picker.truncate_start = false;
compositor.push(Box::new(picker))
}
},
@@ -2864,14 +3122,104 @@ pub fn code_action(cx: &mut Context) {
)
}
+pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
+ use lsp::ResourceOp;
+ use std::fs;
+ match op {
+ ResourceOp::Create(op) => {
+ let path = op.uri.to_file_path().unwrap();
+ let ignore_if_exists = if let Some(options) = &op.options {
+ !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
+ } else {
+ false
+ };
+ if ignore_if_exists && path.exists() {
+ Ok(())
+ } else {
+ fs::write(&path, [])
+ }
+ }
+ ResourceOp::Delete(op) => {
+ let path = op.uri.to_file_path().unwrap();
+ if path.is_dir() {
+ let recursive = if let Some(options) = &op.options {
+ options.recursive.unwrap_or(false)
+ } else {
+ false
+ };
+ if recursive {
+ fs::remove_dir_all(&path)
+ } else {
+ fs::remove_dir(&path)
+ }
+ } else if path.is_file() {
+ fs::remove_file(&path)
+ } else {
+ Ok(())
+ }
+ }
+ ResourceOp::Rename(op) => {
+ let from = op.old_uri.to_file_path().unwrap();
+ let to = op.new_uri.to_file_path().unwrap();
+ let ignore_if_exists = if let Some(options) = &op.options {
+ !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
+ } else {
+ false
+ };
+ if ignore_if_exists && to.exists() {
+ Ok(())
+ } else {
+ fs::rename(&from, &to)
+ }
+ }
+ }
+}
+
fn apply_workspace_edit(
editor: &mut Editor,
offset_encoding: OffsetEncoding,
workspace_edit: &lsp::WorkspaceEdit,
) {
+ let mut apply_edits = |uri: &helix_lsp::Url, text_edits: Vec<lsp::TextEdit>| {
+ let path = uri
+ .to_file_path()
+ .expect("unable to convert URI to filepath");
+
+ let current_view_id = view!(editor).id;
+ let doc_id = editor.open(path, Action::Load).unwrap();
+ let doc = editor
+ .document_mut(doc_id)
+ .expect("Document for document_changes not found");
+
+ // Need to determine a view for apply/append_changes_to_history
+ let selections = doc.selections();
+ let view_id = if selections.contains_key(&current_view_id) {
+ // use current if possible
+ current_view_id
+ } else {
+ // Hack: we take the first available view_id
+ selections
+ .keys()
+ .next()
+ .copied()
+ .expect("No view_id available")
+ };
+
+ let transaction = helix_lsp::util::generate_transaction_from_edits(
+ doc.text(),
+ text_edits,
+ offset_encoding,
+ );
+ doc.apply(&transaction, view_id);
+ doc.append_changes_to_history(view_id);
+ };
+
if let Some(ref changes) = workspace_edit.changes {
log::debug!("workspace changes: {:?}", changes);
- editor.set_error(String::from("Handling workspace_edit.changes is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
+ for (uri, text_edits) in changes {
+ let text_edits = text_edits.to_vec();
+ apply_edits(uri, text_edits);
+ }
return;
// Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used
// TODO: find some example that uses workspace changes, and test it
@@ -2889,30 +3237,6 @@ fn apply_workspace_edit(
match document_changes {
lsp::DocumentChanges::Edits(document_edits) => {
for document_edit in document_edits {
- let path = document_edit
- .text_document
- .uri
- .to_file_path()
- .expect("unable to convert URI to filepath");
- let current_view_id = view!(editor).id;
- let doc = editor
- .document_by_path_mut(path)
- .expect("Document for document_changes not found");
-
- // Need to determine a view for apply/append_changes_to_history
- let selections = doc.selections();
- let view_id = if selections.contains_key(&current_view_id) {
- // use current if possible
- current_view_id
- } else {
- // Hack: we take the first available view_id
- selections
- .keys()
- .next()
- .copied()
- .expect("No view_id available")
- };
-
let edits = document_edit
.edits
.iter()
@@ -2924,19 +3248,33 @@ fn apply_workspace_edit(
})
.cloned()
.collect();
-
- let transaction = helix_lsp::util::generate_transaction_from_edits(
- doc.text(),
- edits,
- offset_encoding,
- );
- doc.apply(&transaction, view_id);
- doc.append_changes_to_history(view_id);
+ apply_edits(&document_edit.text_document.uri, edits);
}
}
lsp::DocumentChanges::Operations(operations) => {
log::debug!("document changes - operations: {:?}", operations);
- editor.set_error(String::from("Handling document operations is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
+ for operateion in operations {
+ match operateion {
+ lsp::DocumentChangeOperation::Op(op) => {
+ apply_document_resource_op(op).unwrap();
+ }
+
+ lsp::DocumentChangeOperation::Edit(document_edit) => {
+ let edits = document_edit
+ .edits
+ .iter()
+ .map(|edit| match edit {
+ lsp::OneOf::Left(text_edit) => text_edit,
+ lsp::OneOf::Right(annotated_text_edit) => {
+ &annotated_text_edit.text_edit
+ }
+ })
+ .cloned()
+ .collect();
+ apply_edits(&document_edit.text_document.uri, edits);
+ }
+ }
+ }
}
}
}
@@ -3175,6 +3513,19 @@ fn goto_last_accessed_file(cx: &mut Context) {
}
}
+fn goto_last_modification(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+ let pos = doc.history.get_mut().last_edit_pos();
+ let text = doc.text().slice(..);
+ if let Some(pos) = pos {
+ let selection = doc
+ .selection(view.id)
+ .clone()
+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
+ doc.set_selection(view.id, selection);
+ }
+}
+
fn select_mode(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
@@ -3773,19 +4124,70 @@ pub mod insert {
doc.apply(&transaction, view.id);
}
- // TODO: handle indent-aware delete
pub fn delete_char_backward(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
+ let indent_unit = doc.indent_unit();
+ let tab_size = doc.tab_width();
+
let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
let pos = range.cursor(text);
- (
- graphemes::nth_prev_grapheme_boundary(text, pos, count),
- pos,
- None,
- )
+ let line_start_pos = text.line_to_char(range.cursor_line(text));
+ // considier to delete by indent level if all characters before `pos` are indent units.
+ let fragment = Cow::from(text.slice(line_start_pos..pos));
+ if !fragment.is_empty() && fragment.chars().all(|ch| ch.is_whitespace()) {
+ if text.get_char(pos.saturating_sub(1)) == Some('\t') {
+ // fast path, delete one char
+ (
+ graphemes::nth_prev_grapheme_boundary(text, pos, 1),
+ pos,
+ None,
+ )
+ } else {
+ let unit_len = indent_unit.chars().count();
+ // NOTE: indent_unit always contains 'only spaces' or 'only tab' according to `IndentStyle` definition.
+ let unit_size = if indent_unit.starts_with('\t') {
+ tab_size * unit_len
+ } else {
+ unit_len
+ };
+ let width: usize = fragment
+ .chars()
+ .map(|ch| {
+ if ch == '\t' {
+ tab_size
+ } else {
+ // it can be none if it still meet control characters other than '\t'
+ // here just set the width to 1 (or some value better?).
+ ch.width().unwrap_or(1)
+ }
+ })
+ .sum();
+ let mut drop = width % unit_size; // round down to nearest unit
+ if drop == 0 {
+ drop = unit_size
+ }; // if it's already at a unit, consume a whole unit
+ let mut chars = fragment.chars().rev();
+ let mut start = pos;
+ for _ in 0..drop {
+ // delete up to `drop` spaces
+ match chars.next() {
+ Some(' ') => start -= 1,
+ _ => break,
+ }
+ }
+ (start, pos, None) // delete!
+ }
+ } else {
+ // delete char
+ (
+ graphemes::nth_prev_grapheme_boundary(text, pos, count),
+ pos,
+ None,
+ )
+ }
});
doc.apply(&transaction, view.id);
}
@@ -3815,8 +4217,19 @@ pub mod insert {
.selection(view.id)
.clone()
.transform(|range| movement::move_prev_word_start(text, range, count));
- doc.set_selection(view.id, selection);
- delete_selection(cx)
+ delete_selection_insert_mode(doc, view, &selection);
+ }
+
+ pub fn delete_word_forward(cx: &mut Context) {
+ let count = cx.count();
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+
+ let selection = doc
+ .selection(view.id)
+ .clone()
+ .transform(|range| movement::move_next_word_start(text, range, count));
+ delete_selection_insert_mode(doc, view, &selection);
}
}
@@ -3826,20 +4239,48 @@ pub mod insert {
// storing it?
fn undo(cx: &mut Context) {
+ let count = cx.count();
let (view, doc) = current!(cx.editor);
- let view_id = view.id;
- let success = doc.undo(view_id);
- if !success {
- cx.editor.set_status("Already at oldest change".to_owned());
+ for _ in 0..count {
+ if !doc.undo(view.id) {
+ cx.editor.set_status("Already at oldest change".to_owned());
+ break;
+ }
}
}
fn redo(cx: &mut Context) {
+ let count = cx.count();
let (view, doc) = current!(cx.editor);
- let view_id = view.id;
- let success = doc.redo(view_id);
- if !success {
- cx.editor.set_status("Already at newest change".to_owned());
+ for _ in 0..count {
+ if !doc.redo(view.id) {
+ cx.editor.set_status("Already at newest change".to_owned());
+ break;
+ }
+ }
+}
+
+fn earlier(cx: &mut Context) {
+ let count = cx.count();
+ let (view, doc) = current!(cx.editor);
+ for _ in 0..count {
+ // rather than doing in batch we do this so get error halfway
+ if !doc.earlier(view.id, UndoKind::Steps(1)) {
+ cx.editor.set_status("Already at oldest change".to_owned());
+ break;
+ }
+ }
+}
+
+fn later(cx: &mut Context) {
+ let count = cx.count();
+ let (view, doc) = current!(cx.editor);
+ for _ in 0..count {
+ // rather than doing in batch we do this so get error halfway
+ if !doc.later(view.id, UndoKind::Steps(1)) {
+ cx.editor.set_status("Already at newest change".to_owned());
+ break;
+ }
}
}
@@ -4297,12 +4738,12 @@ fn join_selections(cx: &mut Context) {
doc.append_changes_to_history(view.id);
}
-fn keep_selections(cx: &mut Context) {
- // keep selections matching regex
+fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
+ // keep or remove selections matching regex
let reg = cx.register.unwrap_or('/');
let prompt = ui::regex_prompt(
cx,
- "keep:".into(),
+ if !remove { "keep:" } else { "remove:" }.into(),
Some(reg),
|_input: &str| Vec::new(),
move |view, doc, regex, event| {
@@ -4311,7 +4752,9 @@ fn keep_selections(cx: &mut Context) {
}
let text = doc.text().slice(..);
- if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), &regex) {
+ if let Some(selection) =
+ selection::keep_or_remove_matches(text, doc.selection(view.id), &regex, remove)
+ {
doc.set_selection(view.id, selection);
}
},
@@ -4320,6 +4763,14 @@ fn keep_selections(cx: &mut Context) {
cx.push_layer(Box::new(prompt));
}
+fn keep_selections(cx: &mut Context) {
+ keep_or_remove_selections_impl(cx, false)
+}
+
+fn remove_selections(cx: &mut Context) {
+ keep_or_remove_selections_impl(cx, true)
+}
+
fn keep_primary_selection(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
// TODO: handle count
@@ -4445,19 +4896,15 @@ pub fn completion(cx: &mut Context) {
return;
}
let size = compositor.size();
- let ui = compositor
- .find(std::any::type_name::<ui::EditorView>())
- .unwrap();
- if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() {
- ui.set_completion(
- editor,
- items,
- offset_encoding,
- start_offset,
- trigger_offset,
- size,
- );
- };
+ let ui = compositor.find::<ui::EditorView>().unwrap();
+ ui.set_completion(
+ editor,
+ items,
+ offset_encoding,
+ start_offset,
+ trigger_offset,
+ size,
+ );
},
);
}
@@ -4487,18 +4934,27 @@ fn hover(cx: &mut Context) {
move |editor: &mut Editor, compositor: &mut Compositor, response: Option<lsp::Hover>| {
if let Some(hover) = response {
// hover.contents / .range <- used for visualizing
- let contents = match hover.contents {
- lsp::HoverContents::Scalar(contents) => {
- // markedstring(string/languagestring to be highlighted)
- // TODO
- log::error!("hover contents {:?}", contents);
- return;
- }
- lsp::HoverContents::Array(contents) => {
- log::error!("hover contents {:?}", contents);
- return;
+
+ fn marked_string_to_markdown(contents: lsp::MarkedString) -> String {
+ match contents {
+ lsp::MarkedString::String(contents) => contents,
+ lsp::MarkedString::LanguageString(string) => {
+ if string.language == "markdown" {
+ string.value
+ } else {
+ format!("```{}\n{}\n```", string.language, string.value)
+ }
+ }
}
- // TODO: render markdown
+ }
+
+ let contents = match hover.contents {
+ lsp::HoverContents::Scalar(contents) => marked_string_to_markdown(contents),
+ lsp::HoverContents::Array(contents) => contents
+ .into_iter()
+ .map(marked_string_to_markdown)
+ .collect::<Vec<_>>()
+ .join("\n\n"),
lsp::HoverContents::Markup(contents) => contents.value,
};
@@ -4706,7 +5162,21 @@ fn wclose(cx: &mut Context) {
}
let view_id = view!(cx.editor).id;
// close current split
- cx.editor.close(view_id, /* close_buffer */ false);
+ cx.editor.close(view_id);
+}
+
+fn wonly(cx: &mut Context) {
+ let views = cx
+ .editor
+ .tree
+ .views()
+ .map(|(v, focus)| (v.id, focus))
+ .collect::<Vec<_>>();
+ for (view_id, focus) in views {
+ if !focus {
+ cx.editor.close(view_id);
+ }
+ }
}
fn select_register(cx: &mut Context) {
@@ -4717,6 +5187,15 @@ fn select_register(cx: &mut Context) {
})
}
+fn insert_register(cx: &mut Context) {
+ cx.on_next_key(move |cx, event| {
+ if let Some(ch) = event.char() {
+ cx.editor.selected_register = Some(ch);
+ paste_before(cx);
+ }
+ })
+}
+
fn align_view_top(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
align_view(doc, view, Align::Top);
@@ -4785,10 +5264,19 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
let selection = doc.selection(view.id).clone().transform(|range| {
match ch {
- 'w' => textobject::textobject_word(text, range, objtype, count),
+ 'w' => textobject::textobject_word(text, range, objtype, count, false),
+ 'W' => textobject::textobject_word(text, range, objtype, count, true),
'c' => textobject_treesitter("class", range),
'f' => textobject_treesitter("function", range),
'p' => textobject_treesitter("parameter", range),
+ 'm' => {
+ let ch = text.char(range.cursor(text));
+ if !ch.is_ascii_alphanumeric() {
+ textobject::textobject_surround(text, range, objtype, ch, count)
+ } else {
+ range
+ }
+ }
// TODO: cancel new ranges if inconsistent surround matches across lines
ch if !ch.is_ascii_alphanumeric() => {
textobject::textobject_surround(text, range, objtype, ch, count)
@@ -5051,6 +5539,10 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
}
+
+ // after replace cursor may be out of bounds, do this to
+ // make sure cursor is in view and update scroll as well
+ view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
},
None,
);
@@ -5096,3 +5588,76 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view.id);
}
+
+fn rename_symbol(cx: &mut Context) {
+ let prompt = Prompt::new(
+ "rename-to:".into(),
+ None,
+ |_input: &str| Vec::new(),
+ move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
+ if event != PromptEvent::Validate {
+ return;
+ }
+
+ log::debug!("renaming to: {:?}", input);
+
+ let (view, doc) = current!(cx.editor);
+ let language_server = match doc.language_server() {
+ Some(language_server) => language_server,
+ None => return,
+ };
+
+ let offset_encoding = language_server.offset_encoding();
+
+ let pos = pos_to_lsp_pos(
+ doc.text(),
+ doc.selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..)),
+ offset_encoding,
+ );
+
+ let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string());
+ let edits = block_on(task).unwrap_or_default();
+ log::debug!("Edits from LSP: {:?}", edits);
+ apply_workspace_edit(&mut cx.editor, offset_encoding, &edits);
+ },
+ None,
+ );
+ cx.push_layer(Box::new(prompt));
+}
+
+/// Increment object under cursor by count.
+fn increment(cx: &mut Context) {
+ increment_impl(cx, cx.count() as i64);
+}
+
+/// Decrement object under cursor by count.
+fn decrement(cx: &mut Context) {
+ increment_impl(cx, -(cx.count() as i64));
+}
+
+/// Decrement object under cursor by `amount`.
+fn increment_impl(cx: &mut Context, amount: i64) {
+ let (view, doc) = current!(cx.editor);
+ let selection = doc.selection(view.id);
+ let text = doc.text();
+
+ let changes = selection.ranges().iter().filter_map(|range| {
+ let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?;
+ let new_text = incrementor.incremented_text(amount);
+ Some((
+ incrementor.range.from(),
+ incrementor.range.to(),
+ Some(new_text),
+ ))
+ });
+
+ if changes.clone().count() > 0 {
+ let transaction = Transaction::change(doc.text(), changes);
+ let transaction = transaction.with_selection(selection.clone());
+
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+ }
+}
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index dc8b91d7..3a644750 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -177,11 +177,12 @@ impl Compositor {
.any(|component| component.type_name() == type_name)
}
- pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> {
+ pub fn find<T: 'static>(&mut self) -> Option<&mut T> {
+ let type_name = std::any::type_name::<T>();
self.layers
.iter_mut()
.find(|component| component.type_name() == type_name)
- .map(|component| component.as_mut())
+ .and_then(|component| component.as_any_mut().downcast_mut())
}
}
diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs
index 13917656..3745f871 100644
--- a/helix-term/src/config.rs
+++ b/helix-term/src/config.rs
@@ -3,6 +3,7 @@ use serde::Deserialize;
use crate::keymap::Keymaps;
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
+#[serde(deny_unknown_fields)]
pub struct Config {
pub theme: Option<String>,
#[serde(default)]
@@ -14,7 +15,7 @@ pub struct Config {
}
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
-#[serde(rename_all = "kebab-case")]
+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LspConfig {
pub display_messages: bool,
}
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index fd56a5c9..42a62fc2 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -25,6 +25,54 @@ macro_rules! key {
};
}
+#[macro_export]
+macro_rules! shift {
+ ($key:ident) => {
+ ::helix_view::input::KeyEvent {
+ code: ::helix_view::keyboard::KeyCode::$key,
+ modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
+ }
+ };
+ ($($ch:tt)*) => {
+ ::helix_view::input::KeyEvent {
+ code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
+ modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
+ }
+ };
+}
+
+#[macro_export]
+macro_rules! ctrl {
+ ($key:ident) => {
+ ::helix_view::input::KeyEvent {
+ code: ::helix_view::keyboard::KeyCode::$key,
+ modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
+ }
+ };
+ ($($ch:tt)*) => {
+ ::helix_view::input::KeyEvent {
+ code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
+ modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
+ }
+ };
+}
+
+#[macro_export]
+macro_rules! alt {
+ ($key:ident) => {
+ ::helix_view::input::KeyEvent {
+ code: ::helix_view::keyboard::KeyCode::$key,
+ modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
+ }
+ };
+ ($($ch:tt)*) => {
+ ::helix_view::input::KeyEvent {
+ code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
+ modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
+ }
+ };
+}
+
/// Macro for defining the root of a `Keymap` object. Example:
///
/// ```
@@ -53,6 +101,10 @@ macro_rules! keymap {
keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
};
+ (@trie [$($cmd:ident),* $(,)?]) => {
+ $crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*])
+ };
+
(
{ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
) => {
@@ -64,10 +116,11 @@ macro_rules! keymap {
$(
$(
let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
- _map.insert(
+ let _duplicate = _map.insert(
_key,
keymap!(@trie $value)
);
+ debug_assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
_order.push(_key);
)+
)*
@@ -147,6 +200,7 @@ impl KeyTrieNode {
cmd.doc()
}
KeyTrie::Node(n) => n.name(),
+ KeyTrie::Sequence(_) => "[Multiple commands]",
};
match body.iter().position(|(d, _)| d == &desc) {
Some(pos) => {
@@ -207,6 +261,7 @@ impl DerefMut for KeyTrieNode {
#[serde(untagged)]
pub enum KeyTrie {
Leaf(Command),
+ Sequence(Vec<Command>),
Node(KeyTrieNode),
}
@@ -214,14 +269,14 @@ impl KeyTrie {
pub fn node(&self) -> Option<&KeyTrieNode> {
match *self {
KeyTrie::Node(ref node) => Some(node),
- KeyTrie::Leaf(_) => None,
+ KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
}
}
pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
match *self {
KeyTrie::Node(ref mut node) => Some(node),
- KeyTrie::Leaf(_) => None,
+ KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
}
}
@@ -238,7 +293,7 @@ impl KeyTrie {
trie = match trie {
KeyTrie::Node(map) => map.get(key),
// leaf encountered while keys left to process
- KeyTrie::Leaf(_) => None,
+ KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
}?
}
Some(trie)
@@ -250,6 +305,8 @@ pub enum KeymapResultKind {
/// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode),
Matched(Command),
+ /// Matched a sequence of commands to execute.
+ MatchedSequence(Vec<Command>),
/// Key was not found in the root keymap
NotFound,
/// Key is invalid in combination with previous keys. Contains keys leading upto
@@ -332,6 +389,12 @@ impl Keymap {
Some(&KeyTrie::Leaf(cmd)) => {
return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
}
+ Some(&KeyTrie::Sequence(ref cmds)) => {
+ return KeymapResult::new(
+ KeymapResultKind::MatchedSequence(cmds.clone()),
+ self.sticky(),
+ )
+ }
None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()),
Some(t) => t,
};
@@ -349,6 +412,13 @@ impl Keymap {
self.state.clear();
return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
}
+ Some(&KeyTrie::Sequence(ref cmds)) => {
+ self.state.clear();
+ KeymapResult::new(
+ KeymapResultKind::MatchedSequence(cmds.clone()),
+ self.sticky(),
+ )
+ }
None => KeymapResult::new(
KeymapResultKind::Cancelled(self.state.drain(..).collect()),
self.sticky(),
@@ -455,6 +525,7 @@ impl Default for Keymaps {
"a" => goto_last_accessed_file,
"n" => goto_next_buffer,
"p" => goto_previous_buffer,
+ "." => goto_last_modification,
},
":" => command_mode,
@@ -511,6 +582,8 @@ impl Default for Keymaps {
"u" => undo,
"U" => redo,
+ "A-u" => earlier,
+ "A-U" => later,
"y" => yank,
// yank_all
@@ -523,7 +596,7 @@ impl Default for Keymaps {
"=" => format_selections,
"J" => join_selections,
"K" => keep_selections,
- // TODO: and another method for inverse
+ "A-K" => remove_selections,
"," => keep_primary_selection,
"A-," => remove_primary_selection,
@@ -532,7 +605,7 @@ impl Default for Keymaps {
// "Q" => replay_macro,
// & align selections
- // _ trim selections
+ "_" => trim_selections,
"(" => rotate_selections_backward,
")" => rotate_selections_forward,
@@ -550,6 +623,7 @@ impl Default for Keymaps {
"C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
"C-q" | "q" => wclose,
+ "C-o" | "o" => wonly,
"C-h" | "h" | "left" => jump_view_left,
"C-j" | "j" | "down" => jump_view_down,
"C-k" | "k" | "up" => jump_view_up,
@@ -569,6 +643,7 @@ impl Default for Keymaps {
"f" => file_picker,
"b" => buffer_picker,
"s" => symbol_picker,
+ "S" => workspace_symbol_picker,
"a" => code_action,
"'" => last_picker,
"d" => { "Debug" sticky=true
@@ -593,9 +668,14 @@ impl Default for Keymaps {
},
"w" => { "Window"
"C-w" | "w" => rotate_view,
- "C-h" | "h" => hsplit,
+ "C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
"C-q" | "q" => wclose,
+ "C-o" | "o" => wonly,
+ "C-h" | "h" | "left" => jump_view_left,
+ "C-j" | "j" | "down" => jump_view_down,
+ "C-k" | "k" | "up" => jump_view_up,
+ "C-l" | "l" | "right" => jump_view_right,
},
"y" => yank_joined_to_clipboard,
"Y" => yank_main_selection_to_clipboard,
@@ -604,30 +684,31 @@ impl Default for Keymaps {
"R" => replace_selections_with_clipboard,
"/" => global_search,
"k" => hover,
+ "r" => rename_symbol,
},
"z" => { "View"
"z" | "c" => align_view_center,
"t" => align_view_top,
"b" => align_view_bottom,
"m" => align_view_middle,
- "k" => scroll_up,
- "j" => scroll_down,
- "b" => page_up,
- "f" => page_down,
- "u" => half_page_up,
- "d" => half_page_down,
+ "k" | "up" => scroll_up,
+ "j" | "down" => scroll_down,
+ "C-b" | "pageup" => page_up,
+ "C-f" | "pagedown" => page_down,
+ "C-u" => half_page_up,
+ "C-d" => half_page_down,
},
"Z" => { "View" sticky=true
"z" | "c" => align_view_center,
"t" => align_view_top,
"b" => align_view_bottom,
"m" => align_view_middle,
- "k" => scroll_up,
- "j" => scroll_down,
- "b" => page_up,
- "f" => page_down,
- "u" => half_page_up,
- "d" => half_page_down,
+ "k" | "up" => scroll_up,
+ "j" | "down" => scroll_down,
+ "C-b" | "pageup" => page_up,
+ "C-f" | "pagedown" => page_down,
+ "C-u" => half_page_up,
+ "C-d" => half_page_down,
},
"\"" => select_register,
@@ -637,6 +718,9 @@ impl Default for Keymaps {
"A-!" => shell_append_output,
"$" => shell_keep_pipe,
"C-z" => suspend,
+
+ "C-a" => increment,
+ "C-x" => decrement,
});
let mut select = normal.clone();
select.merge_nodes(keymap!({ "Select mode"
@@ -670,21 +754,38 @@ impl Default for Keymaps {
"esc" => normal_mode,
"backspace" => delete_char_backward,
+ "C-h" => delete_char_backward,
"del" => delete_char_forward,
+ "C-d" => delete_char_forward,
"ret" => insert_newline,
"tab" => insert_tab,
"C-w" => delete_word_backward,
+ "A-d" => delete_word_forward,
"left" => move_char_left,
+ "C-b" => move_char_left,
"down" => move_line_down,
+ "C-n" => move_line_down,
"up" => move_line_up,
+ "C-p" => move_line_up,
"right" => move_char_right,
+ "C-f" => move_char_right,
+ "A-b" => move_prev_word_end,
+ "A-left" => move_prev_word_end,
+ "A-f" => move_next_word_start,
+ "A-right" => move_next_word_start,
"pageup" => page_up,
"pagedown" => page_down,
"home" => goto_line_start,
+ "C-a" => goto_line_start,
"end" => goto_line_end_newline,
+ "C-e" => goto_line_end_newline,
+
+ "C-k" => kill_to_line_end,
+ "C-u" => kill_to_line_start,
"C-x" => completion,
+ "C-r" => insert_register,
});
Keymaps(hashmap!(
Mode::Normal => Keymap::new(normal),
@@ -706,6 +807,22 @@ pub fn merge_keys(mut config: Config) -> Config {
#[cfg(test)]
mod tests {
use super::*;
+
+ #[test]
+ #[should_panic]
+ fn duplicate_keys_should_panic() {
+ keymap!({ "Normal mode"
+ "i" => normal_mode,
+ "i" => goto_definition,
+ });
+ }
+
+ #[test]
+ fn check_duplicate_keys_in_default_keymap() {
+ // will panic on duplicate keys, assumes that `Keymaps` uses keymap! macro
+ Keymaps::default();
+ }
+
#[test]
fn merge_partial_keys() {
let config = Config {
@@ -800,4 +917,20 @@ mod tests {
let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
assert!(!node.node().unwrap().order().is_empty())
}
+
+ #[test]
+ fn aliased_modes_are_same_in_default_keymap() {
+ let keymaps = Keymaps::default();
+ let root = keymaps.get(&Mode::Normal).unwrap().root();
+ assert_eq!(
+ root.search(&[key!(' '), key!('w')]).unwrap(),
+ root.search(&["C-w".parse::<KeyEvent>().unwrap()]).unwrap(),
+ "Mismatch for window mode on `Space-w` and `Ctrl-w`"
+ );
+ assert_eq!(
+ root.search(&[key!('z')]).unwrap(),
+ root.search(&[key!('Z')]).unwrap(),
+ "Mismatch for view mode on `z` and `Z`"
+ );
+ }
}
diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs
index f746895c..88140130 100644
--- a/helix-term/src/main.rs
+++ b/helix-term/src/main.rs
@@ -16,11 +16,6 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
};
// Separate file config so we can include year, month and day in file logs
- let file = std::fs::OpenOptions::new()
- .write(true)
- .create(true)
- .truncate(true)
- .open(logpath)?;
let file_config = fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
@@ -31,15 +26,20 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
message
))
})
- .chain(file);
+ .chain(fern::log_file(logpath)?);
base_config.chain(file_config).apply()?;
Ok(())
}
+fn main() -> Result<()> {
+ let exit_code = main_impl()?;
+ std::process::exit(exit_code);
+}
+
#[tokio::main]
-async fn main() -> Result<()> {
+async fn main_impl() -> Result<i32> {
let cache_dir = helix_core::cache_dir();
if !cache_dir.exists() {
std::fs::create_dir_all(&cache_dir).ok();
@@ -66,7 +66,7 @@ FLAGS:
-V, --version Prints version information
",
env!("CARGO_PKG_NAME"),
- env!("CARGO_PKG_VERSION"),
+ env!("VERSION_AND_GIT_HASH"),
env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"),
logpath.display(),
@@ -81,7 +81,7 @@ FLAGS:
}
if args.display_version {
- println!("helix {}", env!("CARGO_PKG_VERSION"));
+ println!("helix {}", env!("VERSION_AND_GIT_HASH"));
std::process::exit(0);
}
@@ -91,7 +91,16 @@ FLAGS:
}
let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
- Ok(config) => merge_keys(toml::from_str(&config)?),
+ Ok(config) => toml::from_str(&config)
+ .map(merge_keys)
+ .unwrap_or_else(|err| {
+ eprintln!("Bad config: {}", err);
+ eprintln!("Press <ENTER> to continue with default config");
+ use std::io::Read;
+ // This waits for an enter press.
+ let _ = std::io::stdin().read(&mut []);
+ Config::default()
+ }),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
Err(err) => return Err(Error::new(err)),
};
@@ -100,7 +109,8 @@ FLAGS:
// TODO: use the thread local executor to spawn the application task separately from the work pool
let mut app = Application::new(args, config).context("unable to create new application")?;
- app.run().await.unwrap();
- Ok(())
+ let exit_code = app.run().await?;
+
+ Ok(exit_code)
}
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 2ee7f0ea..01554c64 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -17,7 +17,7 @@ use helix_core::{
};
use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame};
use helix_view::{
- document::Mode,
+ document::{Mode, SCRATCH_BUFFER_NAME},
editor::LineNumber,
graphics::{Color, CursorKind, Modifier, Rect, Style},
info::Info,
@@ -610,7 +610,7 @@ impl EditorView {
use tui::{
layout::Alignment,
text::Text,
- widgets::{Paragraph, Widget},
+ widgets::{Paragraph, Widget, Wrap},
};
let cursor = doc
@@ -665,8 +665,10 @@ impl EditorView {
}
}
- let paragraph = Paragraph::new(lines).alignment(Alignment::Right);
- let width = 80.min(viewport.width);
+ let paragraph = Paragraph::new(lines)
+ .alignment(Alignment::Right)
+ .wrap(Wrap { trim: true });
+ let width = 100.min(viewport.width);
let height = 15.min(viewport.height);
paragraph.render(
Rect::new(viewport.right() - width, viewport.y + 1, width, height),
@@ -716,18 +718,20 @@ impl EditorView {
}
surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
- if let Some(path) = doc.relative_path() {
- let path = path.to_string_lossy();
+ 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,
- );
- }
+ 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.
@@ -830,6 +834,11 @@ impl EditorView {
match &key_result.kind {
KeymapResultKind::Matched(command) => command.execute(cxt),
KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
+ KeymapResultKind::MatchedSequence(commands) => {
+ for command in commands {
+ command.execute(cxt);
+ }
+ }
KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result),
}
None
@@ -871,7 +880,7 @@ impl EditorView {
std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
}
// special handling for repeat operator
- key!('.') => {
+ key!('.') if self.keymaps.pending().is_empty() => {
// first execute whatever put us into insert mode
self.last_insert.0.execute(cxt);
// then replay the inputs
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index 4144ed3c..61630d55 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -13,7 +13,7 @@ use helix_core::{
Rope,
};
use helix_view::{
- graphics::{Color, Margin, Rect, Style},
+ graphics::{Margin, Rect},
Theme,
};
@@ -61,9 +61,15 @@ fn parse<'a>(
})
}
- let text_style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender
- let code_style = Style::default().fg(Color::Rgb(255, 255, 255)); // white
- let heading_style = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
+ 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 {
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index 3c492d14..e891c149 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -1,5 +1,8 @@
-use crate::compositor::{Component, Compositor, Context, EventResult};
-use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
+use crate::{
+ compositor::{Component, Compositor, Context, EventResult},
+ ctrl, key, shift,
+};
+use crossterm::event::Event;
use tui::{buffer::Buffer as Surface, widgets::Table};
pub use tui::widgets::{Cell, Row};
@@ -192,63 +195,25 @@ impl<T: Item + 'static> Component for Menu<T> {
compositor.pop();
})));
- match event {
+ match event.into() {
// esc or ctrl-c aborts the completion and closes the menu
- KeyEvent {
- code: KeyCode::Esc, ..
- }
- | KeyEvent {
- code: KeyCode::Char('c'),
- modifiers: KeyModifiers::CONTROL,
- } => {
+ key!(Esc) | ctrl!('c') => {
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort);
return close_fn;
}
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
- KeyEvent {
- code: KeyCode::BackTab,
- ..
- }
- | KeyEvent {
- code: KeyCode::Up, ..
- }
- | KeyEvent {
- code: KeyCode::Char('p'),
- modifiers: KeyModifiers::CONTROL,
- }
- | KeyEvent {
- code: KeyCode::Char('k'),
- modifiers: KeyModifiers::CONTROL,
- } => {
+ shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
return EventResult::Consumed(None);
}
- // arrow down/ctrl-n/tab advances completion choice (including updating the doc)
- KeyEvent {
- code: KeyCode::Tab,
- modifiers: KeyModifiers::NONE,
- }
- | KeyEvent {
- code: KeyCode::Down,
- ..
- }
- | KeyEvent {
- code: KeyCode::Char('n'),
- modifiers: KeyModifiers::CONTROL,
- }
- | KeyEvent {
- code: KeyCode::Char('j'),
- modifiers: KeyModifiers::CONTROL,
- } => {
+ key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
+ // arrow down/ctrl-n/tab advances completion choice (including updating the doc)
self.move_down();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
return EventResult::Consumed(None);
}
- KeyEvent {
- code: KeyCode::Enter,
- ..
- } => {
+ key!(Enter) => {
if let Some(selection) = self.selection() {
(self.callback_fn)(cx.editor, Some(selection), MenuEvent::Validate);
}
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index d634bc4a..0915937d 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -35,6 +35,7 @@ pub fn regex_prompt(
let (view, doc) = current!(cx.editor);
let view_id = view.id;
let snapshot = doc.selection(view_id).clone();
+ let offset_snapshot = view.offset;
Prompt::new(
prompt,
@@ -45,6 +46,7 @@ pub fn regex_prompt(
PromptEvent::Abort => {
let (view, doc) = current!(cx.editor);
doc.set_selection(view.id, snapshot.clone());
+ view.offset = offset_snapshot;
}
PromptEvent::Validate => {
// TODO: push_jump to store selection just before jump
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 01e5f6c3..2b2e47a4 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -1,8 +1,9 @@
use crate::{
compositor::{Component, Compositor, Context, EventResult},
+ ctrl, key, shift,
ui::EditorView,
};
-use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
+use crossterm::event::Event;
use tui::{
buffer::Buffer as Surface,
widgets::{Block, BorderType, Borders},
@@ -36,6 +37,7 @@ type FileLocation = (PathBuf, Option<(usize, usize)>);
pub struct FilePicker<T> {
picker: Picker<T>,
+ pub truncate_start: bool,
/// Caches paths to documents
preview_cache: HashMap<PathBuf, CachedPreview>,
read_buffer: Vec<u8>,
@@ -89,6 +91,7 @@ impl<T> FilePicker<T> {
) -> Self {
Self {
picker: Picker::new(false, options, format_fn, callback_fn),
+ truncate_start: true,
preview_cache: HashMap::new(),
read_buffer: Vec::with_capacity(1024),
file_fn: Box::new(preview_fn),
@@ -171,6 +174,7 @@ impl<T: 'static> Component for FilePicker<T> {
};
let picker_area = area.with_width(picker_width);
+ self.picker.truncate_start = self.truncate_start;
self.picker.render(picker_area, surface, cx);
if !render_preview {
@@ -276,6 +280,8 @@ pub struct Picker<T> {
prompt: Prompt,
/// Whether to render in the middle of the area
render_centered: bool,
+ /// Wheather to truncate the start (default true)
+ pub truncate_start: bool,
format_fn: Box<dyn Fn(&T) -> Cow<str>>,
callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
@@ -306,6 +312,7 @@ impl<T> Picker<T> {
cursor: 0,
prompt,
render_centered,
+ truncate_start: true,
format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn),
};
@@ -403,81 +410,35 @@ impl<T: 'static> Component for Picker<T> {
compositor.last_picker = compositor.pop();
})));
- match key_event {
- KeyEvent {
- code: KeyCode::Up, ..
- }
- | KeyEvent {
- code: KeyCode::BackTab,
- ..
- }
- | KeyEvent {
- code: KeyCode::Char('k'),
- modifiers: KeyModifiers::CONTROL,
- }
- | KeyEvent {
- code: KeyCode::Char('p'),
- modifiers: KeyModifiers::CONTROL,
- } => {
+ match key_event.into() {
+ shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up();
}
- KeyEvent {
- code: KeyCode::Down,
- ..
- }
- | KeyEvent {
- code: KeyCode::Tab, ..
- }
- | KeyEvent {
- code: KeyCode::Char('j'),
- modifiers: KeyModifiers::CONTROL,
- }
- | KeyEvent {
- code: KeyCode::Char('n'),
- modifiers: KeyModifiers::CONTROL,
- } => {
+ key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
self.move_down();
}
- KeyEvent {
- code: KeyCode::Esc, ..
- }
- | KeyEvent {
- code: KeyCode::Char('c'),
- modifiers: KeyModifiers::CONTROL,
- } => {
+ key!(Esc) | ctrl!('c') => {
return close_fn;
}
- KeyEvent {
- code: KeyCode::Enter,
- ..
- } => {
+ key!(Enter) => {
if let Some(option) = self.selection() {
(self.callback_fn)(cx, option, Action::Replace);
}
return close_fn;
}
- KeyEvent {
- code: KeyCode::Char('s'),
- modifiers: KeyModifiers::CONTROL,
- } => {
+ ctrl!('s') => {
if let Some(option) = self.selection() {
(self.callback_fn)(cx, option, Action::HorizontalSplit);
}
return close_fn;
}
- KeyEvent {
- code: KeyCode::Char('v'),
- modifiers: KeyModifiers::CONTROL,
- } => {
+ ctrl!('v') => {
if let Some(option) = self.selection() {
(self.callback_fn)(cx, option, Action::VerticalSplit);
}
return close_fn;
}
- KeyEvent {
- code: KeyCode::Char(' '),
- modifiers: KeyModifiers::CONTROL,
- } => {
+ ctrl!(' ') => {
self.save_filter();
}
_ => {
@@ -567,7 +528,7 @@ impl<T: 'static> Component for Picker<T> {
text_style
},
true,
- true,
+ self.truncate_start,
);
}
}
diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs
index 1bab1eae..8f7921a1 100644
--- a/helix-term/src/ui/popup.rs
+++ b/helix-term/src/ui/popup.rs
@@ -1,5 +1,8 @@
-use crate::compositor::{Component, Compositor, Context, EventResult};
-use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
+use crate::{
+ compositor::{Component, Compositor, Context, EventResult},
+ ctrl, key,
+};
+use crossterm::event::Event;
use tui::buffer::Buffer as Surface;
use helix_core::Position;
@@ -95,27 +98,14 @@ impl<T: Component> Component for Popup<T> {
compositor.pop();
})));
- match key {
+ match key.into() {
// esc or ctrl-c aborts the completion and closes the menu
- KeyEvent {
- code: KeyCode::Esc, ..
- }
- | KeyEvent {
- code: KeyCode::Char('c'),
- modifiers: KeyModifiers::CONTROL,
- } => close_fn,
-
- KeyEvent {
- code: KeyCode::Char('d'),
- modifiers: KeyModifiers::CONTROL,
- } => {
+ key!(Esc) | ctrl!('c') => close_fn,
+ ctrl!('d') => {
self.scroll(self.size.1 as usize / 2, true);
EventResult::Consumed(None)
}
- KeyEvent {
- code: KeyCode::Char('u'),
- modifiers: KeyModifiers::CONTROL,
- } => {
+ ctrl!('u') => {
self.scroll(self.size.1 as usize / 2, false);
EventResult::Consumed(None)
}
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index 593fd934..00ffdccf 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -1,6 +1,8 @@
use crate::compositor::{Component, Compositor, Context, EventResult};
-use crate::ui;
-use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
+use crate::{alt, ctrl, key, shift, ui};
+use crossterm::event::Event;
+use helix_view::input::KeyEvent;
+use helix_view::keyboard::{KeyCode, KeyModifiers};
use std::{borrow::Cow, ops::RangeFrom};
use tui::buffer::Buffer as Surface;
@@ -213,6 +215,14 @@ impl Prompt {
self.completion = (self.completion_fn)(&self.line);
}
+ pub fn delete_char_forwards(&mut self) {
+ let pos = self.eval_movement(Movement::ForwardChar(1));
+ self.line.replace_range(self.cursor..pos, "");
+
+ self.exit_selection();
+ self.completion = (self.completion_fn)(&self.line);
+ }
+
pub fn delete_word_backwards(&mut self) {
let pos = self.eval_movement(Movement::BackwardWord(1));
self.line.replace_range(pos..self.cursor, "");
@@ -222,6 +232,23 @@ impl Prompt {
self.completion = (self.completion_fn)(&self.line);
}
+ pub fn delete_word_forwards(&mut self) {
+ let pos = self.eval_movement(Movement::ForwardWord(1));
+ self.line.replace_range(self.cursor..pos, "");
+
+ self.exit_selection();
+ self.completion = (self.completion_fn)(&self.line);
+ }
+
+ pub fn kill_to_start_of_line(&mut self) {
+ let pos = self.eval_movement(Movement::StartOfLine);
+ self.line.replace_range(pos..self.cursor, "");
+ self.cursor = pos;
+
+ self.exit_selection();
+ self.completion = (self.completion_fn)(&self.line);
+ }
+
pub fn kill_to_end_of_line(&mut self) {
let pos = self.eval_movement(Movement::EndOfLine);
self.line.replace_range(self.cursor..pos, "");
@@ -405,84 +432,30 @@ impl Component for Prompt {
compositor.pop();
})));
- match event {
- KeyEvent {
- code: KeyCode::Char('c'),
- modifiers: KeyModifiers::CONTROL,
- }
- | KeyEvent {
- code: KeyCode::Esc, ..
- } => {
+ match event.into() {
+ ctrl!('c') | key!(Esc) => {
(self.callback_fn)(cx, &self.line, PromptEvent::Abort);
return close_fn;
}
- KeyEvent {
- code: KeyCode::Left,
- modifiers: KeyModifiers::ALT,
- }
- | KeyEvent {
- code: KeyCode::Char('b'),
- modifiers: KeyModifiers::ALT,
- } => self.move_cursor(Movement::BackwardWord(1)),
- KeyEvent {
- code: KeyCode::Right,
- modifiers: KeyModifiers::ALT,
- }
- | KeyEvent {
- code: KeyCode::Char('f'),
- modifiers: KeyModifiers::ALT,
- } => self.move_cursor(Movement::ForwardWord(1)),
- KeyEvent {
- code: KeyCode::Char('f'),
- modifiers: KeyModifiers::CONTROL,
- }
- | KeyEvent {
- code: KeyCode::Right,
- ..
- } => self.move_cursor(Movement::ForwardChar(1)),
- KeyEvent {
- code: KeyCode::Char('b'),
- modifiers: KeyModifiers::CONTROL,
- }
- | KeyEvent {
- code: KeyCode::Left,
- ..
- } => self.move_cursor(Movement::BackwardChar(1)),
- KeyEvent {
- code: KeyCode::End,
- modifiers: KeyModifiers::NONE,
- }
- | KeyEvent {
- code: KeyCode::Char('e'),
- modifiers: KeyModifiers::CONTROL,
- } => self.move_end(),
- KeyEvent {
- code: KeyCode::Home,
- modifiers: KeyModifiers::NONE,
- }
- | KeyEvent {
- code: KeyCode::Char('a'),
- modifiers: KeyModifiers::CONTROL,
- } => self.move_start(),
- KeyEvent {
- code: KeyCode::Char('w'),
- modifiers: KeyModifiers::CONTROL,
- } => self.delete_word_backwards(),
- KeyEvent {
- code: KeyCode::Char('k'),
- modifiers: KeyModifiers::CONTROL,
- } => self.kill_to_end_of_line(),
- KeyEvent {
- code: KeyCode::Backspace,
- modifiers: KeyModifiers::NONE,
- } => {
+ alt!('b') | alt!(Left) => self.move_cursor(Movement::BackwardWord(1)),
+ alt!('f') | alt!(Right) => self.move_cursor(Movement::ForwardWord(1)),
+ ctrl!('b') | key!(Left) => self.move_cursor(Movement::BackwardChar(1)),
+ ctrl!('f') | key!(Right) => self.move_cursor(Movement::ForwardChar(1)),
+ ctrl!('e') | key!(End) => self.move_end(),
+ ctrl!('a') | key!(Home) => self.move_start(),
+ ctrl!('w') => self.delete_word_backwards(),
+ alt!('d') => self.delete_word_forwards(),
+ ctrl!('k') => self.kill_to_end_of_line(),
+ ctrl!('u') => self.kill_to_start_of_line(),
+ ctrl!('h') | key!(Backspace) => {
self.delete_char_backwards();
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
- KeyEvent {
- code: KeyCode::Char('s'),
- modifiers: KeyModifiers::CONTROL,
- } => {
+ ctrl!('d') | key!(Delete) => {
+ self.delete_char_forwards();
+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
+ }
+ ctrl!('s') => {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
@@ -492,6 +465,7 @@ impl Component for Prompt {
doc.selection(view.id).primary(),
textobject::TextObject::Inside,
1,
+ false,
);
let line = text.slice(range.from()..range.to()).to_string();
if !line.is_empty() {
@@ -499,10 +473,7 @@ impl Component for Prompt {
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
}
- KeyEvent {
- code: KeyCode::Enter,
- ..
- } => {
+ key!(Enter) => {
if self.selection.is_some() && self.line.ends_with('/') {
self.completion = (self.completion_fn)(&self.line);
self.exit_selection();
@@ -517,50 +488,29 @@ impl Component for Prompt {
return close_fn;
}
}
- KeyEvent {
- code: KeyCode::Char('p'),
- modifiers: KeyModifiers::CONTROL,
- }
- | KeyEvent {
- code: KeyCode::Up, ..
- } => {
+ ctrl!('p') | key!(Up) => {
if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
}
- KeyEvent {
- code: KeyCode::Char('n'),
- modifiers: KeyModifiers::CONTROL,
- }
- | KeyEvent {
- code: KeyCode::Down,
- ..
- } => {
+ ctrl!('n') | key!(Down) => {
if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
}
- KeyEvent {
- code: KeyCode::Tab, ..
- } => {
+ key!(Tab) => {
self.change_completion_selection(CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
- KeyEvent {
- code: KeyCode::BackTab,
- ..
- } => {
+ shift!(BackTab) => {
self.change_completion_selection(CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
- KeyEvent {
- code: KeyCode::Char('q'),
- modifiers: KeyModifiers::CONTROL,
- } => self.exit_selection(),
+ ctrl!('q') => self.exit_selection(),
// any char event that's not combined with control or mapped to any other combo
KeyEvent {
code: KeyCode::Char(c),