aboutsummaryrefslogtreecommitdiff
path: root/helix-term
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term')
-rw-r--r--helix-term/Cargo.toml10
-rw-r--r--helix-term/src/application.rs79
-rw-r--r--helix-term/src/commands.rs506
-rw-r--r--helix-term/src/keymap.rs8
-rw-r--r--helix-term/src/ui/completion.rs40
-rw-r--r--helix-term/src/ui/editor.rs99
-rw-r--r--helix-term/src/ui/mod.rs29
-rw-r--r--helix-term/src/ui/prompt.rs4
8 files changed, 576 insertions, 199 deletions
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index 623c5bb9..28b4fe2a 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"
@@ -22,12 +22,12 @@ 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-core = { version = "0.6", path = "../helix-core" }
+helix-view = { version = "0.6", path = "../helix-view" }
+helix-lsp = { version = "0.6", path = "../helix-lsp" }
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"
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 3e0b6d59..c7202feb 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -1,8 +1,12 @@
use helix_core::{merge_toml_values, syntax};
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
use helix_view::{theme, Editor};
+use serde_json::json;
-use crate::{args::Args, compositor::Compositor, config::Config, job::Jobs, ui};
+use crate::{
+ args::Args, commands::apply_workspace_edit, compositor::Compositor, config::Config, job::Jobs,
+ ui,
+};
use log::{error, warn};
@@ -374,6 +378,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
@@ -411,19 +416,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
})
@@ -530,14 +547,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 => {
@@ -567,8 +576,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),
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index cd566720..5c26a5b2 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -26,6 +26,7 @@ use helix_view::{
};
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},
@@ -266,6 +267,7 @@ impl MappableCommand {
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",
@@ -287,7 +289,7 @@ impl MappableCommand {
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 selection",
goto_file_hsplit, "Goto files in selection (hsplit)",
@@ -360,6 +362,7 @@ impl MappableCommand {
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",
jump_forward, "Jump forward on jumplist",
jump_backward, "Jump backward on jumplist",
save_selection, "Save the current selection to the jumplist",
@@ -396,7 +399,7 @@ impl MappableCommand {
increment, "Increment",
decrement, "Decrement",
record_macro, "Record macro",
- play_macro, "Play macro",
+ replay_macro, "Replay macro",
);
}
@@ -1280,16 +1283,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) {
@@ -1543,7 +1553,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
let reg = cx.register.unwrap_or('/');
let scrolloff = cx.editor.config.scrolloff;
- let (_, doc) = current!(cx.editor);
+ let doc = doc!(cx.editor);
// TODO: could probably share with select_on_matches?
@@ -1630,7 +1640,7 @@ 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);
}
@@ -1904,7 +1914,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);
}
@@ -1938,7 +1962,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);
}
@@ -2030,7 +2054,7 @@ pub mod cmd {
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().as_ref()))
@@ -2083,8 +2107,7 @@ pub mod cmd {
_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);
@@ -2307,12 +2330,7 @@ pub mod cmd {
write_all_impl(cx, args, event, true, true)
}
- fn quit_all_impl(
- editor: &mut Editor,
- _args: &[Cow<str>],
- _event: PromptEvent,
- force: bool,
- ) -> anyhow::Result<()> {
+ fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> {
if !force {
buffers_remaining_impl(editor)?;
}
@@ -2328,18 +2346,18 @@ pub mod cmd {
fn quit_all(
cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
) -> anyhow::Result<()> {
- quit_all_impl(cx.editor, args, event, false)
+ quit_all_impl(cx.editor, false)
}
fn force_quit_all(
cx: &mut compositor::Context,
- args: &[Cow<str>],
- event: PromptEvent,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
) -> anyhow::Result<()> {
- quit_all_impl(cx.editor, args, event, true)
+ quit_all_impl(cx.editor, true)
}
fn cquit(
@@ -2353,12 +2371,21 @@ pub 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(
@@ -2393,7 +2420,7 @@ pub mod cmd {
args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let (_, doc) = current!(cx.editor);
+ 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)
@@ -2412,7 +2439,7 @@ pub mod cmd {
args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let (_, doc) = current!(cx.editor);
+ 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)
@@ -2539,7 +2566,7 @@ pub mod cmd {
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 {
@@ -2637,6 +2664,86 @@ pub mod cmd {
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()?,
+ "smart-case" => runtime_config.smart_case = 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()?,
+ _ => 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_slice(&fragment))
+ .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(())
}
@@ -2664,18 +2771,18 @@ pub 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",
@@ -2783,6 +2890,13 @@ pub 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 editor theme.",
@@ -2928,7 +3042,28 @@ pub 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,
+ },
];
pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
@@ -2948,17 +3083,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();
@@ -3002,7 +3148,16 @@ fn command_mode(cx: &mut Context) {
// Handle typable commands
if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
- let args = shellwords::shellwords(input);
+ 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));
}
@@ -3026,7 +3181,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));
}
@@ -3118,7 +3274,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,
@@ -3139,7 +3295,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)
@@ -3181,17 +3337,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,
@@ -3277,12 +3431,19 @@ pub fn code_action(cx: &mut Context) {
move |editor, code_action, _action| match code_action {
lsp::CodeActionOrCommand::Command(command) => {
log::debug!("code action command: {:?}", command);
- editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
+ 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 {
- apply_workspace_edit(editor, offset_encoding, workspace_edit)
+ log::debug!("edit: {:?}", workspace_edit);
+ apply_workspace_edit(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());
}
}
},
@@ -3293,6 +3454,25 @@ pub fn code_action(cx: &mut Context) {
)
}
+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;
@@ -3346,7 +3526,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,
@@ -3537,22 +3717,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(),
)
};
@@ -3563,8 +3743,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);
@@ -3610,6 +3792,7 @@ fn normal_mode(cx: &mut Context) {
doc.mode = Mode::Normal;
+ try_restore_indent(doc, view.id);
doc.append_changes_to_history(view.id);
// if leaving append mode, move cursor back by 1
@@ -3627,6 +3810,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.len32() as usize == 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);
@@ -3994,27 +4211,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) {
@@ -4270,48 +4481,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);
}
@@ -5079,7 +5290,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;
@@ -5257,6 +5468,7 @@ 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)
}
@@ -5272,7 +5484,39 @@ 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);
+ 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);
doc.set_selection(view.id, selection);
}
};
@@ -5920,42 +6164,42 @@ fn record_macro(cx: &mut Context) {
keys.pop();
let s = keys
.into_iter()
- .map(|key| format!("{}", key))
- .collect::<Vec<_>>()
- .join(" ");
+ .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));
+ .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));
+ .set_status(format!("Recording to register [{}]", reg));
}
}
-fn play_macro(cx: &mut Context) {
+fn replay_macro(cx: &mut Context) {
let reg = cx.register.unwrap_or('@');
- let keys = match cx
- .editor
- .registers
- .get(reg)
- .and_then(|reg| reg.read().get(0))
- .context("Register empty")
- .and_then(|s| {
- s.split_whitespace()
- .map(str::parse::<KeyEvent>)
- .collect::<Result<Vec<_>, _>>()
- .context("Failed to parse macro")
- }) {
- Ok(keys) => keys,
- Err(e) => {
- cx.editor.set_error(format!("{}", e));
- return;
+ 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();
+ let count = cx.count();
cx.callback = Some(Box::new(
move |compositor: &mut Compositor, cx: &mut compositor::Context| {
for _ in 0..count {
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 257d5f29..49f8469a 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -569,11 +569,13 @@ impl Default for Keymaps {
"d" => goto_prev_diag,
"D" => goto_first_diag,
"space" => add_newline_above,
+ "o" => shrink_selection,
},
"]" => { "Right bracket"
"d" => goto_next_diag,
"D" => goto_last_diag,
"space" => add_newline_below,
+ "o" => expand_selection,
},
"/" => search,
@@ -593,8 +595,8 @@ impl Default for Keymaps {
// paste_all
"P" => paste_before,
- "q" => record_macro,
- "Q" => play_macro,
+ "Q" => record_macro,
+ "q" => replay_macro,
">" => indent,
"<" => unindent,
@@ -617,6 +619,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,
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index a55201ff..274330c0 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 {
+ Completion::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(),
@@ -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();
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 6b015171..5b7e9075 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -7,7 +7,7 @@ use crate::{
};
use helix_core::{
- coords_at_pos,
+ coords_at_pos, encoding,
graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary},
movement::Direction,
syntax::{self, HighlightEvent},
@@ -566,21 +566,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.
//-------------------------------
@@ -654,6 +639,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
@@ -664,6 +656,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
@@ -782,8 +799,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
}
@@ -941,14 +959,18 @@ impl EditorView {
}
impl Component for EditorView {
- fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
- let mut cxt = commands::Context {
- editor: 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 {
@@ -958,18 +980,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 => {
@@ -981,8 +1004,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);
@@ -992,40 +1015,40 @@ 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);
// mode transitions
match (mode, doc.mode()) {
@@ -1054,7 +1077,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),
}
}
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index f57e2e2b..9ff9118f 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -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;
@@ -208,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());
@@ -256,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/prompt.rs b/helix-term/src/ui/prompt.rs
index 07e1b33c..0202de23 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
@@ -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 {