From a1207fd7683c2e038df923704bb5790c6cdaefea Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Tue, 9 Nov 2021 17:06:40 -0800 Subject: [PATCH 001/186] helix-term/commands: display buffer id in picker --- helix-term/src/commands.rs | 4 ++-- helix-view/src/lib.rs | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fa1fa4e41..e2c4a9d9d 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3385,7 +3385,7 @@ fn buffer_picker(cx: &mut Context) { .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), + None => SCRATCH_BUFFER_NAME, }; let mut flags = Vec::new(); @@ -3401,7 +3401,7 @@ fn buffer_picker(cx: &mut Context) { } else { format!(" ({})", flags.join("")) }; - Cow::Owned(format!("{}{}", path, flag)) + Cow::Owned(format!("{} {}{}", self.id, path, flag)) } } diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index a56c914d4..e0964e1ca 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -26,6 +26,12 @@ impl Default for DocumentId { } } +impl std::fmt::Display for DocumentId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.0)) + } +} + slotmap::new_key_type! { pub struct ViewId; } From 6118486eb2dd7ed680ff38a7cc024dbfb26fbb7f Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Sat, 20 Nov 2021 13:03:39 -0800 Subject: [PATCH 002/186] helix-term: implement buffer completer In order to implement this completer, the completion function needs to be able to access the compositor's context (to allow it to get the list of buffers currently open in the context's editor). --- helix-term/src/commands.rs | 72 +++++++++++++++++++++++----------- helix-term/src/commands/dap.rs | 7 ++-- helix-term/src/ui/mod.rs | 47 +++++++++++++++++++--- helix-term/src/ui/picker.rs | 8 ++-- helix-term/src/ui/prompt.rs | 55 +++++++++++++------------- 5 files changed, 127 insertions(+), 62 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e2c4a9d9d..bf87f446f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1435,7 +1435,7 @@ fn select_regex(cx: &mut Context) { cx, "select:".into(), Some(reg), - |_input: &str| Vec::new(), + |_ctx: &compositor::Context, _input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1458,7 +1458,7 @@ fn split_selection(cx: &mut Context) { cx, "split:".into(), Some(reg), - |_input: &str| Vec::new(), + |_ctx: &compositor::Context, _input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1600,7 +1600,7 @@ fn searcher(cx: &mut Context, direction: Direction) { cx, "search:".into(), Some(reg), - move |input: &str| { + move |_ctx: &compositor::Context, input: &str| { completions .iter() .filter(|comp| comp.starts_with(input)) @@ -1701,7 +1701,7 @@ fn global_search(cx: &mut Context) { cx, "global-search:".into(), None, - move |input: &str| { + move |_ctx: &compositor::Context, input: &str| { completions .iter() .filter(|comp| comp.starts_with(input)) @@ -2079,26 +2079,54 @@ pub mod cmd { Ok(()) } + fn buffer_close_impl( + editor: &mut Editor, + args: &[Cow], + force: bool, + ) -> anyhow::Result<()> { + if args.is_empty() { + let doc_id = view!(editor).doc; + editor.close_document(doc_id, force)?; + return Ok(()); + } + + for arg in args { + let doc_id = editor.documents().find_map(|doc| { + let arg_path = Some(Path::new(arg.as_ref())); + if doc.path().map(|p| p.as_path()) == arg_path + || doc.relative_path().as_deref() == arg_path + { + Some(doc.id()) + } else { + None + } + }); + + match doc_id { + Some(doc_id) => editor.close_document(doc_id, force)?, + None => { + editor.set_error(format!("couldn't close buffer '{}': does not exist", arg)); + } + } + } + + Ok(()) + } + fn buffer_close( cx: &mut compositor::Context, - _args: &[Cow], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - let view = view!(cx.editor); - let doc_id = view.doc; - cx.editor.close_document(doc_id, false)?; - Ok(()) + buffer_close_impl(cx.editor, args, false) } fn force_buffer_close( cx: &mut compositor::Context, - _args: &[Cow], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - let view = view!(cx.editor); - let doc_id = view.doc; - cx.editor.close_document(doc_id, true)?; - Ok(()) + buffer_close_impl(cx.editor, args, true) } fn write_impl(cx: &mut compositor::Context, path: Option<&Cow>) -> anyhow::Result<()> { @@ -2927,14 +2955,14 @@ pub mod cmd { aliases: &["bc", "bclose"], doc: "Close the current buffer.", fun: buffer_close, - completer: None, // FIXME: buffer completer + completer: Some(completers::buffer), }, 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 + completer: Some(completers::buffer), }, TypableCommand { name: "write", @@ -3262,7 +3290,7 @@ fn command_mode(cx: &mut Context) { let mut prompt = Prompt::new( ":".into(), Some(':'), - |input: &str| { + |ctx: &compositor::Context, input: &str| { static FUZZY_MATCHER: Lazy = Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default); @@ -3294,7 +3322,7 @@ fn command_mode(cx: &mut Context) { .. }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) { - completer(part) + completer(ctx, part) .into_iter() .map(|(range, file)| { // offset ranges to input @@ -5358,7 +5386,7 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) { cx, if !remove { "keep:" } else { "remove:" }.into(), Some(reg), - |_input: &str| Vec::new(), + |_ctx: &compositor::Context, _input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -6122,7 +6150,7 @@ fn shell_keep_pipe(cx: &mut Context) { let prompt = Prompt::new( "keep-pipe:".into(), Some('|'), - |_input: &str| Vec::new(), + |_ctx: &compositor::Context, _input: &str| Vec::new(), move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { let shell = &cx.editor.config.shell; if event != PromptEvent::Validate { @@ -6218,7 +6246,7 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { let prompt = Prompt::new( prompt, Some('|'), - |_input: &str| Vec::new(), + |_ctx: &compositor::Context, _input: &str| Vec::new(), move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { let shell = &cx.editor.config.shell; if event != PromptEvent::Validate { @@ -6314,7 +6342,7 @@ fn rename_symbol(cx: &mut Context) { let prompt = Prompt::new( "rename-to:".into(), None, - |_input: &str| Vec::new(), + |_ctx: &compositor::Context, _input: &str| Vec::new(), move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { if event != PromptEvent::Validate { return; diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 9da2715f4..925c65c1d 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -361,8 +361,9 @@ fn debug_parameter_prompt( let completer = match field_type { "filename" => ui::completers::filename, "directory" => ui::completers::directory, - _ => |_input: &str| Vec::new(), + _ => ui::completers::none, }; + Prompt::new( format!("{}: ", name).into(), None, @@ -696,7 +697,7 @@ pub fn dap_edit_condition(cx: &mut Context) { let mut prompt = Prompt::new( "condition:".into(), None, - |_input: &str| Vec::new(), + ui::completers::none, move |cx, input: &str, event: PromptEvent| { if event != PromptEvent::Validate { return; @@ -740,7 +741,7 @@ pub fn dap_edit_log(cx: &mut Context) { let mut prompt = Prompt::new( "log-message:".into(), None, - |_input: &str| Vec::new(), + ui::completers::none, move |cx, input: &str, event: PromptEvent| { if event != PromptEvent::Validate { return; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 7f6d9f7c5..263342b76 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -30,7 +30,7 @@ pub fn regex_prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, history_register: Option, - completion_fn: impl FnMut(&str) -> Vec + 'static, + completion_fn: impl FnMut(&crate::compositor::Context, &str) -> Vec + 'static, fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static, ) -> Prompt { let (view, doc) = current!(cx.editor); @@ -168,18 +168,53 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi } pub mod completers { + use crate::compositor::Context; use crate::ui::prompt::Completion; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; + use helix_view::document::SCRATCH_BUFFER_NAME; use helix_view::editor::Config; use helix_view::theme; use once_cell::sync::Lazy; use std::borrow::Cow; use std::cmp::Reverse; - pub type Completer = fn(&str) -> Vec; + pub type Completer = fn(&Context, &str) -> Vec; - pub fn theme(input: &str) -> Vec { + pub fn none(_cx: &Context, _input: &str) -> Vec { + Vec::new() + } + + pub fn buffer(cx: &Context, input: &str) -> Vec { + let mut names: Vec<_> = cx + .editor + .documents + .iter() + .map(|(_id, doc)| { + let name = doc + .relative_path() + .map(|p| p.display().to_string()) + .unwrap_or_else(|| String::from(SCRATCH_BUFFER_NAME)); + ((0..), Cow::from(name)) + }) + .collect(); + + let matcher = Matcher::default(); + + let mut matches: Vec<_> = names + .into_iter() + .filter_map(|(_range, name)| { + matcher.fuzzy_match(&name, input).map(|score| (name, score)) + }) + .collect(); + + matches.sort_unstable_by_key(|(_file, score)| Reverse(*score)); + names = matches.into_iter().map(|(name, _)| ((0..), name)).collect(); + + names + } + + pub fn theme(_cx: &Context, input: &str) -> Vec { let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes")); names.extend(theme::Loader::read_names( &helix_core::config_dir().join("themes"), @@ -207,7 +242,7 @@ pub mod completers { names } - pub fn setting(input: &str) -> Vec { + pub fn setting(_cx: &Context, input: &str) -> Vec { static KEYS: Lazy> = Lazy::new(|| { serde_json::to_value(Config::default()) .unwrap() @@ -232,7 +267,7 @@ pub mod completers { .collect() } - pub fn filename(input: &str) -> Vec { + pub fn filename(_cx: &Context, input: &str) -> Vec { filename_impl(input, |entry| { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); @@ -244,7 +279,7 @@ pub mod completers { }) } - pub fn directory(input: &str) -> Vec { + pub fn directory(_cx: &Context, input: &str) -> Vec { filename_impl(input, |entry| { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 9cddbc607..dcc640026 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -302,7 +302,7 @@ impl Picker { let prompt = Prompt::new( "".into(), None, - |_pattern: &str| Vec::new(), + |_ctx: &Context, _pattern: &str| Vec::new(), |_editor: &mut Context, _pattern: &str, _event: PromptEvent| { // }, @@ -395,12 +395,12 @@ impl Picker { .map(|(index, _score)| &self.options[*index]) } - pub fn save_filter(&mut self) { + pub fn save_filter(&mut self, cx: &Context) { self.filters.clear(); self.filters .extend(self.matches.iter().map(|(index, _)| *index)); self.filters.sort_unstable(); // used for binary search later - self.prompt.clear(); + self.prompt.clear(cx); } } @@ -468,7 +468,7 @@ impl Component for Picker { return close_fn; } ctrl!(' ') => { - self.save_filter(); + self.save_filter(cx); } _ => { if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 4c4fef268..ff6b8c76e 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -24,7 +24,7 @@ pub struct Prompt { selection: Option, history_register: Option, history_pos: Option, - completion_fn: Box Vec>, + completion_fn: Box Vec>, callback_fn: Box, pub doc_fn: Box Option<&'static str>>, } @@ -59,14 +59,14 @@ impl Prompt { pub fn new( prompt: Cow<'static, str>, history_register: Option, - mut completion_fn: impl FnMut(&str) -> Vec + 'static, + completion_fn: impl FnMut(&Context, &str) -> Vec + 'static, callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static, ) -> Self { Self { prompt, line: String::new(), cursor: 0, - completion: completion_fn(""), + completion: Vec::new(), selection: None, history_register, history_pos: None, @@ -177,13 +177,13 @@ impl Prompt { } } - pub fn insert_char(&mut self, c: char) { + pub fn insert_char(&mut self, c: char, cx: &Context) { self.line.insert(self.cursor, c); let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false); if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { self.cursor = pos; } - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(cx, &self.line); self.exit_selection(); } @@ -205,61 +205,61 @@ impl Prompt { self.cursor = self.line.len(); } - pub fn delete_char_backwards(&mut self) { + pub fn delete_char_backwards(&mut self, cx: &Context) { let pos = self.eval_movement(Movement::BackwardChar(1)); self.line.replace_range(pos..self.cursor, ""); self.cursor = pos; self.exit_selection(); - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(cx, &self.line); } - pub fn delete_char_forwards(&mut self) { + pub fn delete_char_forwards(&mut self, cx: &Context) { 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); + self.completion = (self.completion_fn)(cx, &self.line); } - pub fn delete_word_backwards(&mut self) { + pub fn delete_word_backwards(&mut self, cx: &Context) { let pos = self.eval_movement(Movement::BackwardWord(1)); self.line.replace_range(pos..self.cursor, ""); self.cursor = pos; self.exit_selection(); - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(cx, &self.line); } - pub fn delete_word_forwards(&mut self) { + pub fn delete_word_forwards(&mut self, cx: &Context) { 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); + self.completion = (self.completion_fn)(cx, &self.line); } - pub fn kill_to_start_of_line(&mut self) { + pub fn kill_to_start_of_line(&mut self, cx: &Context) { 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); + self.completion = (self.completion_fn)(cx, &self.line); } - pub fn kill_to_end_of_line(&mut self) { + pub fn kill_to_end_of_line(&mut self, cx: &Context) { let pos = self.eval_movement(Movement::EndOfLine); self.line.replace_range(self.cursor..pos, ""); self.exit_selection(); - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(cx, &self.line); } - pub fn clear(&mut self) { + pub fn clear(&mut self, cx: &Context) { self.line.clear(); self.cursor = 0; - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(cx, &self.line); self.exit_selection(); } @@ -442,16 +442,16 @@ impl Component for Prompt { 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!('w') => self.delete_word_backwards(cx), + alt!('d') => self.delete_word_forwards(cx), + ctrl!('k') => self.kill_to_end_of_line(cx), + ctrl!('u') => self.kill_to_start_of_line(cx), ctrl!('h') | key!(Backspace) => { - self.delete_char_backwards(); + self.delete_char_backwards(cx); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } ctrl!('d') | key!(Delete) => { - self.delete_char_forwards(); + self.delete_char_forwards(cx); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } ctrl!('s') => { @@ -474,7 +474,7 @@ impl Component for Prompt { } key!(Enter) => { if self.selection.is_some() && self.line.ends_with(std::path::MAIN_SEPARATOR) { - self.completion = (self.completion_fn)(&self.line); + self.completion = (self.completion_fn)(cx, &self.line); self.exit_selection(); } else { (self.callback_fn)(cx, &self.line, PromptEvent::Validate); @@ -515,7 +515,7 @@ impl Component for Prompt { code: KeyCode::Char(c), modifiers, } if !modifiers.contains(KeyModifiers::CONTROL) => { - self.insert_char(c); + self.insert_char(c, cx); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } _ => (), @@ -525,6 +525,7 @@ impl Component for Prompt { } fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + self.completion = (self.completion_fn)(cx, &self.line); self.render_prompt(area, surface, cx) } From e023a78919be31a9d9747d3735dfd29dd6c6605d Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Sat, 20 Nov 2021 13:42:40 -0800 Subject: [PATCH 003/186] WIP: show all buffers that couldn't be closed --- helix-term/src/commands.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index bf87f446f..2c1535b69 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2090,6 +2090,7 @@ pub mod cmd { return Ok(()); } + let mut nonexistent_buffers = vec![]; for arg in args { let doc_id = editor.documents().find_map(|doc| { let arg_path = Some(Path::new(arg.as_ref())); @@ -2104,12 +2105,19 @@ pub mod cmd { match doc_id { Some(doc_id) => editor.close_document(doc_id, force)?, - None => { - editor.set_error(format!("couldn't close buffer '{}': does not exist", arg)); - } + None => nonexistent_buffers.push(arg), } } + let nonexistent_buffers: Vec<_> = nonexistent_buffers + .into_iter() + .map(|str| format!("'{}'", str)) + .collect(); + editor.set_error(format!( + "couldn't close buffer(s) {}: does not exist", + nonexistent_buffers.join(", ") + )); + Ok(()) } From af21e2a5b491a18b22de7c99d96536bdaf741056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 17 Feb 2022 13:55:46 +0900 Subject: [PATCH 004/186] Pass through Editor instead of Context --- helix-term/src/commands.rs | 20 ++++++++++---------- helix-term/src/ui/mod.rs | 24 +++++++++++------------- helix-term/src/ui/picker.rs | 4 ++-- helix-term/src/ui/prompt.rs | 27 +++++++++++++++------------ 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 2c1535b69..9cf5630da 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1435,7 +1435,7 @@ fn select_regex(cx: &mut Context) { cx, "select:".into(), Some(reg), - |_ctx: &compositor::Context, _input: &str| Vec::new(), + ui::completers::none, move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1458,7 +1458,7 @@ fn split_selection(cx: &mut Context) { cx, "split:".into(), Some(reg), - |_ctx: &compositor::Context, _input: &str| Vec::new(), + ui::completers::none, move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1600,7 +1600,7 @@ fn searcher(cx: &mut Context, direction: Direction) { cx, "search:".into(), Some(reg), - move |_ctx: &compositor::Context, input: &str| { + move |_editor: &Editor, input: &str| { completions .iter() .filter(|comp| comp.starts_with(input)) @@ -1701,7 +1701,7 @@ fn global_search(cx: &mut Context) { cx, "global-search:".into(), None, - move |_ctx: &compositor::Context, input: &str| { + move |_editor: &Editor, input: &str| { completions .iter() .filter(|comp| comp.starts_with(input)) @@ -3298,7 +3298,7 @@ fn command_mode(cx: &mut Context) { let mut prompt = Prompt::new( ":".into(), Some(':'), - |ctx: &compositor::Context, input: &str| { + |editor: &Editor, input: &str| { static FUZZY_MATCHER: Lazy = Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default); @@ -3330,7 +3330,7 @@ fn command_mode(cx: &mut Context) { .. }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) { - completer(ctx, part) + completer(editor, part) .into_iter() .map(|(range, file)| { // offset ranges to input @@ -5394,7 +5394,7 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) { cx, if !remove { "keep:" } else { "remove:" }.into(), Some(reg), - |_ctx: &compositor::Context, _input: &str| Vec::new(), + ui::completers::none, move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -6158,7 +6158,7 @@ fn shell_keep_pipe(cx: &mut Context) { let prompt = Prompt::new( "keep-pipe:".into(), Some('|'), - |_ctx: &compositor::Context, _input: &str| Vec::new(), + ui::completers::none, move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { let shell = &cx.editor.config.shell; if event != PromptEvent::Validate { @@ -6254,7 +6254,7 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { let prompt = Prompt::new( prompt, Some('|'), - |_ctx: &compositor::Context, _input: &str| Vec::new(), + ui::completers::none, move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { let shell = &cx.editor.config.shell; if event != PromptEvent::Validate { @@ -6350,7 +6350,7 @@ fn rename_symbol(cx: &mut Context) { let prompt = Prompt::new( "rename-to:".into(), None, - |_ctx: &compositor::Context, _input: &str| Vec::new(), + ui::completers::none, move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { if event != PromptEvent::Validate { return; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 263342b76..e9ff9d1a3 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -22,7 +22,7 @@ pub use text::Text; use helix_core::regex::Regex; use helix_core::regex::RegexBuilder; -use helix_view::{Document, View}; +use helix_view::{Document, Editor, View}; use std::path::PathBuf; @@ -30,7 +30,7 @@ pub fn regex_prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, history_register: Option, - completion_fn: impl FnMut(&crate::compositor::Context, &str) -> Vec + 'static, + completion_fn: impl FnMut(&Editor, &str) -> Vec + 'static, fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static, ) -> Prompt { let (view, doc) = current!(cx.editor); @@ -168,26 +168,24 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi } pub mod completers { - use crate::compositor::Context; use crate::ui::prompt::Completion; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::FuzzyMatcher; use helix_view::document::SCRATCH_BUFFER_NAME; - use helix_view::editor::Config; use helix_view::theme; + use helix_view::{editor::Config, Editor}; use once_cell::sync::Lazy; use std::borrow::Cow; use std::cmp::Reverse; - pub type Completer = fn(&Context, &str) -> Vec; + pub type Completer = fn(&Editor, &str) -> Vec; - pub fn none(_cx: &Context, _input: &str) -> Vec { + pub fn none(_editor: &Editor, _input: &str) -> Vec { Vec::new() } - pub fn buffer(cx: &Context, input: &str) -> Vec { - let mut names: Vec<_> = cx - .editor + pub fn buffer(editor: &Editor, input: &str) -> Vec { + let mut names: Vec<_> = editor .documents .iter() .map(|(_id, doc)| { @@ -214,7 +212,7 @@ pub mod completers { names } - pub fn theme(_cx: &Context, input: &str) -> Vec { + pub fn theme(_editor: &Editor, input: &str) -> Vec { let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes")); names.extend(theme::Loader::read_names( &helix_core::config_dir().join("themes"), @@ -242,7 +240,7 @@ pub mod completers { names } - pub fn setting(_cx: &Context, input: &str) -> Vec { + pub fn setting(_editor: &Editor, input: &str) -> Vec { static KEYS: Lazy> = Lazy::new(|| { serde_json::to_value(Config::default()) .unwrap() @@ -267,7 +265,7 @@ pub mod completers { .collect() } - pub fn filename(_cx: &Context, input: &str) -> Vec { + pub fn filename(_editor: &Editor, input: &str) -> Vec { filename_impl(input, |entry| { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); @@ -279,7 +277,7 @@ pub mod completers { }) } - pub fn directory(_cx: &Context, input: &str) -> Vec { + pub fn directory(_editor: &Editor, input: &str) -> Vec { filename_impl(input, |entry| { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index dcc640026..622af3876 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1,7 +1,7 @@ use crate::{ compositor::{Component, Compositor, Context, EventResult}, ctrl, key, shift, - ui::EditorView, + ui::{self, EditorView}, }; use crossterm::event::Event; use tui::{ @@ -302,7 +302,7 @@ impl Picker { let prompt = Prompt::new( "".into(), None, - |_ctx: &Context, _pattern: &str| Vec::new(), + ui::completers::none, |_editor: &mut Context, _pattern: &str, _event: PromptEvent| { // }, diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index ff6b8c76e..18b390dd1 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -24,7 +24,7 @@ pub struct Prompt { selection: Option, history_register: Option, history_pos: Option, - completion_fn: Box Vec>, + completion_fn: Box Vec>, callback_fn: Box, pub doc_fn: Box Option<&'static str>>, } @@ -59,7 +59,7 @@ impl Prompt { pub fn new( prompt: Cow<'static, str>, history_register: Option, - completion_fn: impl FnMut(&Context, &str) -> Vec + 'static, + completion_fn: impl FnMut(&Editor, &str) -> Vec + 'static, callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static, ) -> Self { Self { @@ -76,6 +76,10 @@ impl Prompt { } } + pub fn recalculate_completion(&mut self, editor: &Editor) { + self.completion = (self.completion_fn)(editor, &self.line); + } + /// Compute the cursor position after applying movement /// Taken from: https://github.com/wez/wezterm/blob/e0b62d07ca9bf8ce69a61e30a3c20e7abc48ce7e/termwiz/src/lineedit/mod.rs#L516-L611 fn eval_movement(&self, movement: Movement) -> usize { @@ -183,7 +187,7 @@ impl Prompt { if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { self.cursor = pos; } - self.completion = (self.completion_fn)(cx, &self.line); + self.recalculate_completion(cx.editor); self.exit_selection(); } @@ -211,7 +215,7 @@ impl Prompt { self.cursor = pos; self.exit_selection(); - self.completion = (self.completion_fn)(cx, &self.line); + self.recalculate_completion(cx.editor); } pub fn delete_char_forwards(&mut self, cx: &Context) { @@ -219,7 +223,7 @@ impl Prompt { self.line.replace_range(self.cursor..pos, ""); self.exit_selection(); - self.completion = (self.completion_fn)(cx, &self.line); + self.recalculate_completion(cx.editor); } pub fn delete_word_backwards(&mut self, cx: &Context) { @@ -228,7 +232,7 @@ impl Prompt { self.cursor = pos; self.exit_selection(); - self.completion = (self.completion_fn)(cx, &self.line); + self.recalculate_completion(cx.editor); } pub fn delete_word_forwards(&mut self, cx: &Context) { @@ -236,7 +240,7 @@ impl Prompt { self.line.replace_range(self.cursor..pos, ""); self.exit_selection(); - self.completion = (self.completion_fn)(cx, &self.line); + self.recalculate_completion(cx.editor); } pub fn kill_to_start_of_line(&mut self, cx: &Context) { @@ -245,7 +249,7 @@ impl Prompt { self.cursor = pos; self.exit_selection(); - self.completion = (self.completion_fn)(cx, &self.line); + self.recalculate_completion(cx.editor); } pub fn kill_to_end_of_line(&mut self, cx: &Context) { @@ -253,13 +257,13 @@ impl Prompt { self.line.replace_range(self.cursor..pos, ""); self.exit_selection(); - self.completion = (self.completion_fn)(cx, &self.line); + self.recalculate_completion(cx.editor); } pub fn clear(&mut self, cx: &Context) { self.line.clear(); self.cursor = 0; - self.completion = (self.completion_fn)(cx, &self.line); + self.recalculate_completion(cx.editor); self.exit_selection(); } @@ -474,7 +478,7 @@ impl Component for Prompt { } key!(Enter) => { if self.selection.is_some() && self.line.ends_with(std::path::MAIN_SEPARATOR) { - self.completion = (self.completion_fn)(cx, &self.line); + self.recalculate_completion(cx.editor); self.exit_selection(); } else { (self.callback_fn)(cx, &self.line, PromptEvent::Validate); @@ -525,7 +529,6 @@ impl Component for Prompt { } fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { - self.completion = (self.completion_fn)(cx, &self.line); self.render_prompt(area, surface, cx) } From 24f90ba8d8f4a10fb18f71b05c278fe89b71a261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 17 Feb 2022 13:56:01 +0900 Subject: [PATCH 005/186] Manually recalculate initial completion where it matters --- helix-term/src/commands.rs | 2 ++ helix-term/src/ui/mod.rs | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 9cf5630da..c07f44dcc 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3393,6 +3393,8 @@ fn command_mode(cx: &mut Context) { None }); + // Calculate initial completion + prompt.recalculate_completion(cx.editor); cx.push_layer(Box::new(prompt)); } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index e9ff9d1a3..e269c8edd 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -38,7 +38,7 @@ pub fn regex_prompt( let snapshot = doc.selection(view_id).clone(); let offset_snapshot = view.offset; - Prompt::new( + let mut prompt = Prompt::new( prompt, history_register, completion_fn, @@ -91,7 +91,10 @@ pub fn regex_prompt( } } }, - ) + ); + // Calculate initial completion + prompt.recalculate_completion(cx.editor); + prompt } pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker { From afec54485a3be29ff1172f70157a183853273420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Dzivjak?= Date: Thu, 17 Feb 2022 06:03:11 +0100 Subject: [PATCH 006/186] feat(commands): command palette (#1400) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(commands): command palette Add new command to display command pallete that can be used to discover and execute available commands. Fixes: https://github.com/helix-editor/helix/issues/559 * Make picker take the whole context, not just editor * Bind command pallete * Typable commands also in the palette * Show key bindings for commands * Fix tests, small refactor * Refactor keymap mapping, fix typo * Ignore sequence key bindings for now * Apply suggestions * Fix lint issues in tests * Fix after rebase Co-authored-by: Blaž Hrastnik --- helix-term/src/commands.rs | 66 ++++++++++++++++++++++++++++++- helix-term/src/keymap.rs | 77 ++++++++++++++++++++++++++++++++++++- helix-term/src/ui/editor.rs | 2 +- 3 files changed, 142 insertions(+), 3 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c07f44dcc..bb74f9ece 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -44,7 +44,7 @@ use movement::Movement; use crate::{ args, compositor::{self, Component, Compositor}, - ui::{self, overlay::overlayed, FilePicker, Popup, Prompt, PromptEvent}, + ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, }; use crate::job::{self, Job, Jobs}; @@ -430,6 +430,7 @@ impl MappableCommand { decrement, "Decrement", record_macro, "Record macro", replay_macro, "Replay macro", + command_palette, "Open command pallete", ); } @@ -3692,6 +3693,69 @@ pub fn code_action(cx: &mut Context) { ) } +pub fn command_palette(cx: &mut Context) { + cx.callback = Some(Box::new( + move |compositor: &mut Compositor, cx: &mut compositor::Context| { + let doc = doc_mut!(cx.editor); + let keymap = + compositor.find::().unwrap().keymaps[&doc.mode].reverse_map(); + + let mut commands: Vec = MappableCommand::STATIC_COMMAND_LIST.into(); + commands.extend( + cmd::TYPABLE_COMMAND_LIST + .iter() + .map(|cmd| MappableCommand::Typable { + name: cmd.name.to_owned(), + doc: cmd.doc.to_owned(), + args: Vec::new(), + }), + ); + + // formats key bindings, multiple bindings are comma separated, + // individual key presses are joined with `+` + let fmt_binding = |bindings: &Vec>| -> String { + bindings + .iter() + .map(|bind| { + bind.iter() + .map(|key| key.to_string()) + .collect::>() + .join("+") + }) + .collect::>() + .join(", ") + }; + + let picker = Picker::new( + commands, + move |command| match command { + MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) + { + Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(), + None => doc.into(), + }, + MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { + Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(), + None => (*doc).into(), + }, + }, + move |cx, command, _action| { + let mut ctx = Context { + register: None, + count: std::num::NonZeroUsize::new(1), + editor: cx.editor, + callback: None, + on_next_key_callback: None, + jobs: cx.jobs, + }; + command.execute(&mut ctx); + }, + ); + compositor.push(Box::new(picker)); + }, + )); +} + pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) { let doc = doc!(editor); let language_server = match doc.language_server() { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index f414f797c..0147f58e1 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -343,13 +343,46 @@ pub struct Keymap { impl Keymap { pub fn new(root: KeyTrie) -> Self { - Self { + Keymap { root, state: Vec::new(), sticky: None, } } + pub fn reverse_map(&self) -> HashMap>> { + // recursively visit all nodes in keymap + fn map_node( + cmd_map: &mut HashMap>>, + node: &KeyTrie, + keys: &mut Vec, + ) { + match node { + KeyTrie::Leaf(cmd) => match cmd { + MappableCommand::Typable { name, .. } => { + cmd_map.entry(name.into()).or_default().push(keys.clone()) + } + MappableCommand::Static { name, .. } => cmd_map + .entry(name.to_string()) + .or_default() + .push(keys.clone()), + }, + KeyTrie::Node(next) => { + for (key, trie) in &next.map { + keys.push(*key); + map_node(cmd_map, trie, keys); + keys.pop(); + } + } + KeyTrie::Sequence(_) => {} + }; + } + + let mut res = HashMap::new(); + map_node(&mut res, &self.root, &mut Vec::new()); + res + } + pub fn root(&self) -> &KeyTrie { &self.root } @@ -706,6 +739,7 @@ impl Default for Keymaps { "/" => global_search, "k" => hover, "r" => rename_symbol, + "?" => command_palette, }, "z" => { "View" "z" | "c" => align_view_center, @@ -958,4 +992,45 @@ mod tests { "Mismatch for view mode on `z` and `Z`" ); } + + #[test] + fn reverse_map() { + let normal_mode = keymap!({ "Normal mode" + "i" => insert_mode, + "g" => { "Goto" + "g" => goto_file_start, + "e" => goto_file_end, + }, + "j" | "k" => move_line_down, + }); + let keymap = Keymap::new(normal_mode); + let mut reverse_map = keymap.reverse_map(); + + // sort keybindings in order to have consistent tests + // HashMaps can be compared but we can still get different ordering of bindings + // for commands that have multiple bindings assigned + for v in reverse_map.values_mut() { + v.sort() + } + + assert_eq!( + reverse_map, + HashMap::from([ + ("insert_mode".to_string(), vec![vec![key!('i')]]), + ( + "goto_file_start".to_string(), + vec![vec![key!('g'), key!('g')]] + ), + ( + "goto_file_end".to_string(), + vec![vec![key!('g'), key!('e')]] + ), + ( + "move_line_down".to_string(), + vec![vec![key!('j')], vec![key!('k')]] + ), + ]), + "Mistmatch" + ) + } } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a2131abe3..fc749ebb8 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -31,7 +31,7 @@ use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind}; use tui::buffer::Buffer as Surface; pub struct EditorView { - keymaps: Keymaps, + pub keymaps: Keymaps, on_next_key: Option>, last_insert: (commands::MappableCommand, Vec), pub(crate) completion: Option, From 368064e316ef75076b037af893af3c4eb483c594 Mon Sep 17 00:00:00 2001 From: tomKPZ Date: Thu, 17 Feb 2022 19:13:02 -0800 Subject: [PATCH 007/186] Fix bug when launching hx file.rs:10 (#1676) --- helix-term/src/args.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index 247d5b320..3e50f66f7 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -82,7 +82,7 @@ fn split_path_row_col(s: &str) -> Option<(PathBuf, Position)> { /// /// Does not validate if file.rs is a file or directory. fn split_path_row(s: &str) -> Option<(PathBuf, Position)> { - let (row, path) = s.rsplit_once(':')?; + let (path, row) = s.rsplit_once(':')?; let row: usize = row.parse().ok()?; let path = path.into(); let pos = Position::new(row.saturating_sub(1), 0); From a8cf0c6b90bd469047ac8fc294dbf90fcff71c7a Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 17 Feb 2022 22:05:12 -0600 Subject: [PATCH 008/186] filter git revision on git command success exit code (#1674) The unwrap (or '.ok()' rather) triggers for some errors but not negative status codes. In the case where helix is being packaged in an empty git repository, the existing mechanism will fail because git init git rev-parse HEAD gives a negative exit code and prints to stderr stderr: "fatal: ambiguous argument 'HEAD': unknown revision or path not in the working tree.... with a stdout of "HEAD\n" (too short to slice with [..8]). --- helix-term/build.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/helix-term/build.rs b/helix-term/build.rs index 21dd5612d..b5d62b285 100644 --- a/helix-term/build.rs +++ b/helix-term/build.rs @@ -6,6 +6,7 @@ fn main() { .args(&["rev-parse", "HEAD"]) .output() .ok() + .filter(|output| output.status.success()) .and_then(|x| String::from_utf8(x.stdout).ok()); let version: Cow<_> = match git_hash { From 4e1b3b12f3f5626f1ab371d99fecd23e2f5b8603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 17 Feb 2022 12:22:30 +0900 Subject: [PATCH 009/186] Refactor symbol picker to share code --- helix-term/src/commands.rs | 122 ++++++++++++++++----------------- helix-term/src/commands/dap.rs | 2 +- 2 files changed, 59 insertions(+), 65 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index bb74f9ece..73addcee5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3474,6 +3474,58 @@ fn buffer_picker(cx: &mut Context) { cx.push_layer(Box::new(overlayed(picker))); } +fn sym_picker( + symbols: Vec, + current_path: Option, + offset_encoding: OffsetEncoding, +) -> FilePicker { + // TODO: drop current_path comparison and instead use workspace: bool flag? + let current_path2 = current_path.clone(); + let mut picker = FilePicker::new( + symbols, + move |symbol| { + if current_path.as_ref() == Some(&symbol.location.uri) { + symbol.name.as_str().into() + } else { + let path = symbol.location.uri.to_file_path().unwrap(); + let relative_path = helix_core::path::get_relative_path(path.as_path()) + .to_string_lossy() + .into_owned(); + format!("{} ({})", &symbol.name, relative_path).into() + } + }, + move |cx, symbol, action| { + if current_path2.as_ref() == Some(&symbol.location.uri) { + push_jump(cx.editor); + } else { + 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; + picker +} + fn symbol_picker(cx: &mut Context) { fn nested_to_flat( list: &mut Vec, @@ -3499,6 +3551,7 @@ fn symbol_picker(cx: &mut Context) { Some(language_server) => language_server, None => return, }; + let current_url = doc.url(); let offset_encoding = language_server.offset_encoding(); let future = language_server.document_symbols(doc.identifier()); @@ -3523,32 +3576,7 @@ fn symbol_picker(cx: &mut Context) { } }; - let mut picker = FilePicker::new( - symbols, - |symbol| (&symbol.name).into(), - move |cx, symbol, _action| { - push_jump(cx.editor); - 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; + let picker = sym_picker(symbols, current_url, offset_encoding); compositor.push(Box::new(overlayed(picker))) } }, @@ -3557,7 +3585,7 @@ fn symbol_picker(cx: &mut Context) { fn workspace_symbol_picker(cx: &mut Context) { let doc = doc!(cx.editor); - let current_path = doc.path().cloned(); + let current_url = doc.url(); let language_server = match doc.language_server() { Some(language_server) => language_server, None => return, @@ -3571,43 +3599,7 @@ fn workspace_symbol_picker(cx: &mut Context) { compositor: &mut Compositor, response: Option>| { 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_string_lossy() - .into_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; + let picker = sym_picker(symbols, current_url, offset_encoding); compositor.push(Box::new(overlayed(picker))) } }, @@ -4266,6 +4258,7 @@ fn goto_impl( ) { push_jump(editor); + // TODO: share with symbol picker(symbol.location) fn jump_to( editor: &mut Editor, location: &lsp::Location, @@ -4324,6 +4317,7 @@ fn goto_impl( }, move |cx, location, action| jump_to(cx.editor, location, offset_encoding, action), |_editor, location| { + // TODO: share code for symbol.location and location let path = location.uri.to_file_path().unwrap(); let line = Some(( location.range.start.line as usize, diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 925c65c1d..4de3134b4 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -792,7 +792,7 @@ pub fn dap_switch_stack_frame(cx: &mut Context) { let picker = FilePicker::new( frames, - |frame| frame.name.clone().into(), // TODO: include thread_states in the label + |frame| frame.name.as_str().into(), // TODO: include thread_states in the label move |cx, frame, _action| { let debugger = debugger!(cx.editor); // TODO: this should be simpler to find From 7b1d682fe55cfa72c1ecf5a1763940cf105902e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 17 Feb 2022 12:22:48 +0900 Subject: [PATCH 010/186] dap: fix runInTerminal with lldb-vscode --- helix-term/src/application.rs | 20 +++++++++++++++++++- helix-term/src/commands/dap.rs | 4 ++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 49eb08d0d..db289f57b 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -479,10 +479,28 @@ impl Application { Payload::Response(_) => unreachable!(), Payload::Request(request) => match request.command.as_str() { RunInTerminal::COMMAND => { - let arguments: dap::requests::RunInTerminalArguments = + let mut arguments: dap::requests::RunInTerminalArguments = serde_json::from_value(request.arguments.unwrap_or_default()).unwrap(); // TODO: no unwrap + log::error!("run_in_terminal {:?}", arguments); + + // HAXX: lldb-vscode uses $CWD/lldb-vscode which is wrong + let program = arguments.args[0] + .strip_prefix( + std::env::current_dir() + .expect("Couldn't get current working directory") + .as_path() + .to_str() + .unwrap(), + ) + .and_then(|arg| arg.strip_prefix('/')) + .map(|arg| arg.to_owned()) + .unwrap_or_else(|| arguments.args[0].clone()); + arguments.args[0] = program; + + log::error!("{}", arguments.args.join(" ")); + // TODO: handle cwd let process = std::process::Command::new("tmux") .arg("split-window") diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 4de3134b4..ae76a26ae 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -392,13 +392,13 @@ fn debug_parameter_prompt( Ok(call) }); cx.jobs.callback(callback); - } else if let Err(e) = dap_start_impl( + } else if let Err(err) = dap_start_impl( cx, Some(&config_name), None, Some(params.iter().map(|x| x.into()).collect()), ) { - cx.editor.set_error(e.to_string()); + cx.editor.set_error(err.to_string()); } }, ) From 504d5ce8bd71486fc897ecdb33ff12c1c3a1e6b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 18 Feb 2022 13:58:18 +0900 Subject: [PATCH 011/186] Move most LSP specific commmands to commands::lsp --- helix-term/src/commands.rs | 778 +-------------------------------- helix-term/src/commands/lsp.rs | 776 ++++++++++++++++++++++++++++++++ 2 files changed, 785 insertions(+), 769 deletions(-) create mode 100644 helix-term/src/commands/lsp.rs diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 73addcee5..7e8b1d53e 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,6 +1,8 @@ pub(crate) mod dap; +pub(crate) mod lsp; pub use dap::*; +pub use lsp::*; use helix_core::{ comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, @@ -33,11 +35,6 @@ 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}, - OffsetEncoding, -}; use insert::*; use movement::Movement; @@ -2754,7 +2751,7 @@ pub mod cmd { // TODO: support no frame_id let frame_id = debugger.stack_frames[&thread_id][frame].id; - let response = block_on(debugger.eval(args.join(" "), Some(frame_id)))?; + let response = helix_lsp::block_on(debugger.eval(args.join(" "), Some(frame_id)))?; cx.editor.set_status(response.result); } Ok(()) @@ -3474,217 +3471,6 @@ fn buffer_picker(cx: &mut Context) { cx.push_layer(Box::new(overlayed(picker))); } -fn sym_picker( - symbols: Vec, - current_path: Option, - offset_encoding: OffsetEncoding, -) -> FilePicker { - // TODO: drop current_path comparison and instead use workspace: bool flag? - let current_path2 = current_path.clone(); - let mut picker = FilePicker::new( - symbols, - move |symbol| { - if current_path.as_ref() == Some(&symbol.location.uri) { - symbol.name.as_str().into() - } else { - let path = symbol.location.uri.to_file_path().unwrap(); - let relative_path = helix_core::path::get_relative_path(path.as_path()) - .to_string_lossy() - .into_owned(); - format!("{} ({})", &symbol.name, relative_path).into() - } - }, - move |cx, symbol, action| { - if current_path2.as_ref() == Some(&symbol.location.uri) { - push_jump(cx.editor); - } else { - 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; - picker -} - -fn symbol_picker(cx: &mut Context) { - fn nested_to_flat( - list: &mut Vec, - file: &lsp::TextDocumentIdentifier, - symbol: lsp::DocumentSymbol, - ) { - #[allow(deprecated)] - list.push(lsp::SymbolInformation { - name: symbol.name, - kind: symbol.kind, - tags: symbol.tags, - deprecated: symbol.deprecated, - location: lsp::Location::new(file.uri.clone(), symbol.selection_range), - container_name: None, - }); - for child in symbol.children.into_iter().flatten() { - nested_to_flat(list, file, child); - } - } - let doc = doc!(cx.editor); - - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; - let current_url = doc.url(); - let offset_encoding = language_server.offset_encoding(); - - let future = language_server.document_symbols(doc.identifier()); - - cx.callback( - future, - move |editor: &mut Editor, - compositor: &mut Compositor, - response: Option| { - if let Some(symbols) = response { - // lsp has two ways to represent symbols (flat/nested) - // convert the nested variant to flat, so that we have a homogeneous list - let symbols = match symbols { - lsp::DocumentSymbolResponse::Flat(symbols) => symbols, - lsp::DocumentSymbolResponse::Nested(symbols) => { - let doc = doc!(editor); - let mut flat_symbols = Vec::new(); - for symbol in symbols { - nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol) - } - flat_symbols - } - }; - - let picker = sym_picker(symbols, current_url, offset_encoding); - compositor.push(Box::new(overlayed(picker))) - } - }, - ) -} - -fn workspace_symbol_picker(cx: &mut Context) { - let doc = doc!(cx.editor); - let current_url = doc.url(); - 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()); - - cx.callback( - future, - move |_editor: &mut Editor, - compositor: &mut Compositor, - response: Option>| { - if let Some(symbols) = response { - let picker = sym_picker(symbols, current_url, offset_encoding); - compositor.push(Box::new(overlayed(picker))) - } - }, - ) -} - -impl ui::menu::Item for lsp::CodeActionOrCommand { - fn label(&self) -> &str { - match self { - lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(), - lsp::CodeActionOrCommand::Command(command) => command.title.as_str(), - } - } -} - -pub fn code_action(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; - - let range = range_to_lsp_range( - doc.text(), - doc.selection(view.id).primary(), - language_server.offset_encoding(), - ); - - let future = language_server.code_actions(doc.identifier(), range); - let offset_encoding = language_server.offset_encoding(); - - cx.callback( - future, - move |editor: &mut Editor, - compositor: &mut Compositor, - response: Option| { - let actions = match response { - Some(a) => a, - None => return, - }; - if actions.is_empty() { - editor.set_status("No code actions available"); - return; - } - - let mut picker = ui::Menu::new(actions, move |editor, code_action, event| { - if event != PromptEvent::Validate { - return; - } - - // always present here - let code_action = code_action.unwrap(); - - match code_action { - lsp::CodeActionOrCommand::Command(command) => { - log::debug!("code action command: {:?}", command); - execute_lsp_command(editor, command.clone()); - } - lsp::CodeActionOrCommand::CodeAction(code_action) => { - log::debug!("code action: {:?}", code_action); - if let Some(ref workspace_edit) = code_action.edit { - log::debug!("edit: {:?}", workspace_edit); - apply_workspace_edit(editor, offset_encoding, workspace_edit); - } - - // 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()); - } - } - } - }); - picker.move_down(); // pre-select the first item - - let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin { - vertical: 1, - horizontal: 1, - }); - compositor.replace_or_push("code-action", Box::new(popup)); - }, - ) -} - pub fn command_palette(cx: &mut Context) { cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { @@ -3748,180 +3534,6 @@ pub fn command_palette(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; - match op { - ResourceOp::Create(op) => { - let path = op.uri.to_file_path().unwrap(); - let ignore_if_exists = op.options.as_ref().map_or(false, |options| { - !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(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 = op - .options - .as_ref() - .and_then(|options| options.recursive) - .unwrap_or(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 = op.options.as_ref().map_or(false, |options| { - !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) - }); - if ignore_if_exists && to.exists() { - Ok(()) - } else { - fs::rename(&from, &to) - } - } - } -} - -pub 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| { - 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(¤t_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); - 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 - // for (url, edits) in changes.iter() { - // let file_path = url.origin().ascii_serialization(); - // let file_path = std::path::PathBuf::from(file_path); - // let file = std::fs::File::open(file_path).unwrap(); - // let mut text = Rope::from_reader(file).unwrap(); - // let transaction = edits_to_changes(&text, edits); - // transaction.apply(&mut text); - // } - } - - if let Some(ref document_changes) = workspace_edit.document_changes { - match document_changes { - lsp::DocumentChanges::Edits(document_edits) => { - for document_edit in document_edits { - 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); - } - } - lsp::DocumentChanges::Operations(operations) => { - log::debug!("document changes - operations: {:?}", operations); - 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); - } - } - } - } - } - } -} - fn last_picker(cx: &mut Context) { // TODO: last picker does not seem to work well with buffer_picker cx.callback = Some(Box::new(|compositor: &mut Compositor, _| { @@ -4250,247 +3862,6 @@ fn exit_select_mode(cx: &mut Context) { } } -fn goto_impl( - editor: &mut Editor, - compositor: &mut Compositor, - locations: Vec, - offset_encoding: OffsetEncoding, -) { - push_jump(editor); - - // TODO: share with symbol picker(symbol.location) - fn jump_to( - editor: &mut Editor, - location: &lsp::Location, - offset_encoding: OffsetEncoding, - action: Action, - ) { - let path = location - .uri - .to_file_path() - .expect("unable to convert URI to filepath"); - let _id = editor.open(path, action).expect("editor.open failed"); - let (view, doc) = current!(editor); - let definition_pos = location.range.start; - // TODO: convert inside server - let new_pos = - if let Some(new_pos) = lsp_pos_to_pos(doc.text(), definition_pos, offset_encoding) { - new_pos - } else { - return; - }; - doc.set_selection(view.id, Selection::point(new_pos)); - align_view(doc, view, Align::Center); - } - - let cwdir = std::env::current_dir().expect("couldn't determine current directory"); - - match locations.as_slice() { - [location] => { - jump_to(editor, location, offset_encoding, Action::Replace); - } - [] => { - editor.set_error("No definition found."); - } - _locations => { - let picker = FilePicker::new( - locations, - move |location| { - let file: Cow<'_, str> = (location.uri.scheme() == "file") - .then(|| { - location - .uri - .to_file_path() - .map(|path| { - // strip root prefix - path.strip_prefix(&cwdir) - .map(|path| path.to_path_buf()) - .unwrap_or(path) - }) - .map(|path| Cow::from(path.to_string_lossy().into_owned())) - .ok() - }) - .flatten() - .unwrap_or_else(|| location.uri.as_str().into()); - let line = location.range.start.line; - format!("{}:{}", file, line).into() - }, - move |cx, location, action| jump_to(cx.editor, location, offset_encoding, action), - |_editor, location| { - // TODO: share code for symbol.location and location - let path = location.uri.to_file_path().unwrap(); - let line = Some(( - location.range.start.line as usize, - location.range.end.line as usize, - )); - Some((path, line)) - }, - ); - compositor.push(Box::new(overlayed(picker))); - } - } -} - -fn goto_definition(cx: &mut Context) { - 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 future = language_server.goto_definition(doc.identifier(), pos, None); - - cx.callback( - future, - move |editor: &mut Editor, - compositor: &mut Compositor, - response: Option| { - let items = match response { - Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location], - Some(lsp::GotoDefinitionResponse::Array(locations)) => locations, - Some(lsp::GotoDefinitionResponse::Link(locations)) => locations - .into_iter() - .map(|location_link| lsp::Location { - uri: location_link.target_uri, - range: location_link.target_range, - }) - .collect(), - None => Vec::new(), - }; - - goto_impl(editor, compositor, items, offset_encoding); - }, - ); -} - -fn goto_type_definition(cx: &mut Context) { - 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 future = language_server.goto_type_definition(doc.identifier(), pos, None); - - cx.callback( - future, - move |editor: &mut Editor, - compositor: &mut Compositor, - response: Option| { - let items = match response { - Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location], - Some(lsp::GotoDefinitionResponse::Array(locations)) => locations, - Some(lsp::GotoDefinitionResponse::Link(locations)) => locations - .into_iter() - .map(|location_link| lsp::Location { - uri: location_link.target_uri, - range: location_link.target_range, - }) - .collect(), - None => Vec::new(), - }; - - goto_impl(editor, compositor, items, offset_encoding); - }, - ); -} - -fn goto_implementation(cx: &mut Context) { - 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 future = language_server.goto_implementation(doc.identifier(), pos, None); - - cx.callback( - future, - move |editor: &mut Editor, - compositor: &mut Compositor, - response: Option| { - let items = match response { - Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location], - Some(lsp::GotoDefinitionResponse::Array(locations)) => locations, - Some(lsp::GotoDefinitionResponse::Link(locations)) => locations - .into_iter() - .map(|location_link| lsp::Location { - uri: location_link.target_uri, - range: location_link.target_range, - }) - .collect(), - None => Vec::new(), - }; - - goto_impl(editor, compositor, items, offset_encoding); - }, - ); -} - -fn goto_reference(cx: &mut Context) { - 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 future = language_server.goto_reference(doc.identifier(), pos, None); - - cx.callback( - future, - move |editor: &mut Editor, - compositor: &mut Compositor, - items: Option>| { - goto_impl( - editor, - compositor, - items.unwrap_or_default(), - offset_encoding, - ); - }, - ); -} - fn goto_pos(editor: &mut Editor, pos: usize) { push_jump(editor); @@ -4565,46 +3936,6 @@ fn goto_prev_diag(cx: &mut Context) { goto_pos(editor, pos); } -fn signature_help(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; - - let pos = pos_to_lsp_pos( - doc.text(), - doc.selection(view.id) - .primary() - .cursor(doc.text().slice(..)), - language_server.offset_encoding(), - ); - - let future = language_server.text_document_signature_help(doc.identifier(), pos, None); - - cx.callback( - future, - move |_editor: &mut Editor, - _compositor: &mut Compositor, - response: Option| { - if let Some(signature_help) = response { - log::info!("{:?}", signature_help); - // signatures - // active_signature - // active_parameter - // render as: - - // signature - // ---------- - // doc - - // with active param highlighted - } - }, - ); -} - pub mod insert { use super::*; pub type Hook = fn(&Rope, &Selection, char) -> Option; @@ -4630,6 +3961,7 @@ pub mod insert { } fn language_server_completion(cx: &mut Context, ch: char) { + use helix_lsp::lsp; // if ch matches completion char, trigger completion let doc = doc_mut!(cx.editor); let language_server = match doc.language_server() { @@ -4653,6 +3985,7 @@ pub mod insert { } fn signature_help(cx: &mut Context, ch: char) { + use helix_lsp::lsp; // if ch matches signature_help char, trigger let doc = doc_mut!(cx.editor); let language_server = match doc.language_server() { @@ -5360,6 +4693,8 @@ fn unindent(cx: &mut Context) { } fn format_selections(cx: &mut Context) { + use helix_lsp::{lsp, util::range_to_lsp_range}; + let (view, doc) = current!(cx.editor); // via lsp if available @@ -5504,6 +4839,8 @@ fn remove_primary_selection(cx: &mut Context) { } pub fn completion(cx: &mut Context) { + use helix_lsp::{lsp, util::pos_to_lsp_pos}; + // trigger on trigger char, or if user calls it // (or on word char typing??) // after it's triggered, if response marked is_incomplete, update on every subsequent keypress @@ -5618,66 +4955,6 @@ pub fn completion(cx: &mut Context) { ); } -fn hover(cx: &mut Context) { - let (view, doc) = current!(cx.editor); - - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; - - // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier - - let pos = pos_to_lsp_pos( - doc.text(), - doc.selection(view.id) - .primary() - .cursor(doc.text().slice(..)), - language_server.offset_encoding(), - ); - - let future = language_server.text_document_hover(doc.identifier(), pos, None); - - cx.callback( - future, - move |editor: &mut Editor, compositor: &mut Compositor, response: Option| { - if let Some(hover) = response { - // hover.contents / .range <- used for visualizing - - 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) - } - } - } - } - - 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::>() - .join("\n\n"), - lsp::HoverContents::Markup(contents) => contents.value, - }; - - // skip if contents empty - - let contents = - ui::Markdown::new(contents, editor.syn_loader.clone()).style_group("hover"); - let popup = Popup::new("hover", contents); - compositor.replace_or_push("hover", Box::new(popup)); - } - }, - ); -} - // comments fn toggle_comments(cx: &mut Context) { let (view, doc) = current!(cx.editor); @@ -6406,43 +5683,6 @@ fn add_newline_impl(cx: &mut Context, open: Open) { doc.apply(&transaction, view.id); } -fn rename_symbol(cx: &mut Context) { - let prompt = Prompt::new( - "rename-to:".into(), - None, - ui::completers::none, - 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(cx.editor, offset_encoding, &edits); - }, - ); - cx.push_layer(Box::new(prompt)); -} - /// Increment object under cursor by count. fn increment(cx: &mut Context) { increment_impl(cx, cx.count() as i64); diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs new file mode 100644 index 000000000..084c7c6a8 --- /dev/null +++ b/helix-term/src/commands/lsp.rs @@ -0,0 +1,776 @@ +use helix_lsp::{ + block_on, lsp, + util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range}, + OffsetEncoding, +}; + +use super::{align_view, push_jump, Align, Context, Editor}; + +use helix_core::Selection; +use helix_view::editor::Action; + +use crate::{ + compositor::{self, Compositor}, + ui::{self, overlay::overlayed, FilePicker, Popup, Prompt, PromptEvent}, +}; + +use std::borrow::Cow; + +fn sym_picker( + symbols: Vec, + current_path: Option, + offset_encoding: OffsetEncoding, +) -> FilePicker { + // TODO: drop current_path comparison and instead use workspace: bool flag? + let current_path2 = current_path.clone(); + let mut picker = FilePicker::new( + symbols, + move |symbol| { + if current_path.as_ref() == Some(&symbol.location.uri) { + symbol.name.as_str().into() + } else { + let path = symbol.location.uri.to_file_path().unwrap(); + let relative_path = helix_core::path::get_relative_path(path.as_path()) + .to_string_lossy() + .into_owned(); + format!("{} ({})", &symbol.name, relative_path).into() + } + }, + move |cx, symbol, action| { + if current_path2.as_ref() == Some(&symbol.location.uri) { + push_jump(cx.editor); + } else { + 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; + picker +} + +pub fn symbol_picker(cx: &mut Context) { + fn nested_to_flat( + list: &mut Vec, + file: &lsp::TextDocumentIdentifier, + symbol: lsp::DocumentSymbol, + ) { + #[allow(deprecated)] + list.push(lsp::SymbolInformation { + name: symbol.name, + kind: symbol.kind, + tags: symbol.tags, + deprecated: symbol.deprecated, + location: lsp::Location::new(file.uri.clone(), symbol.selection_range), + container_name: None, + }); + for child in symbol.children.into_iter().flatten() { + nested_to_flat(list, file, child); + } + } + let doc = doc!(cx.editor); + + let language_server = match doc.language_server() { + Some(language_server) => language_server, + None => return, + }; + let current_url = doc.url(); + let offset_encoding = language_server.offset_encoding(); + + let future = language_server.document_symbols(doc.identifier()); + + cx.callback( + future, + move |editor: &mut Editor, + compositor: &mut Compositor, + response: Option| { + if let Some(symbols) = response { + // lsp has two ways to represent symbols (flat/nested) + // convert the nested variant to flat, so that we have a homogeneous list + let symbols = match symbols { + lsp::DocumentSymbolResponse::Flat(symbols) => symbols, + lsp::DocumentSymbolResponse::Nested(symbols) => { + let doc = doc!(editor); + let mut flat_symbols = Vec::new(); + for symbol in symbols { + nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol) + } + flat_symbols + } + }; + + let picker = sym_picker(symbols, current_url, offset_encoding); + compositor.push(Box::new(overlayed(picker))) + } + }, + ) +} + +pub fn workspace_symbol_picker(cx: &mut Context) { + let doc = doc!(cx.editor); + let current_url = doc.url(); + 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()); + + cx.callback( + future, + move |_editor: &mut Editor, + compositor: &mut Compositor, + response: Option>| { + if let Some(symbols) = response { + let picker = sym_picker(symbols, current_url, offset_encoding); + compositor.push(Box::new(overlayed(picker))) + } + }, + ) +} + +impl ui::menu::Item for lsp::CodeActionOrCommand { + fn label(&self) -> &str { + match self { + lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(), + lsp::CodeActionOrCommand::Command(command) => command.title.as_str(), + } + } +} + +pub fn code_action(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + + let language_server = match doc.language_server() { + Some(language_server) => language_server, + None => return, + }; + + let range = range_to_lsp_range( + doc.text(), + doc.selection(view.id).primary(), + language_server.offset_encoding(), + ); + + let future = language_server.code_actions(doc.identifier(), range); + let offset_encoding = language_server.offset_encoding(); + + cx.callback( + future, + move |editor: &mut Editor, + compositor: &mut Compositor, + response: Option| { + let actions = match response { + Some(a) => a, + None => return, + }; + if actions.is_empty() { + editor.set_status("No code actions available"); + return; + } + + let mut picker = ui::Menu::new(actions, move |editor, code_action, event| { + if event != PromptEvent::Validate { + return; + } + + // always present here + let code_action = code_action.unwrap(); + + match code_action { + lsp::CodeActionOrCommand::Command(command) => { + log::debug!("code action command: {:?}", command); + execute_lsp_command(editor, command.clone()); + } + lsp::CodeActionOrCommand::CodeAction(code_action) => { + log::debug!("code action: {:?}", code_action); + if let Some(ref workspace_edit) = code_action.edit { + log::debug!("edit: {:?}", workspace_edit); + apply_workspace_edit(editor, offset_encoding, workspace_edit); + } + + // 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()); + } + } + } + }); + picker.move_down(); // pre-select the first item + + let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin { + vertical: 1, + horizontal: 1, + }); + compositor.replace_or_push("code-action", Box::new(popup)); + }, + ) +} +pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) { + let doc = doc!(editor); + let language_server = match doc.language_server() { + Some(language_server) => language_server, + None => return, + }; + + // the command is executed on the server and communicated back + // to the client asynchronously using workspace edits + let command_future = language_server.command(cmd); + tokio::spawn(async move { + let res = command_future.await; + + if let Err(e) = res { + log::error!("execute LSP command: {}", e); + } + }); +} + +pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> { + use lsp::ResourceOp; + use std::fs; + match op { + ResourceOp::Create(op) => { + let path = op.uri.to_file_path().unwrap(); + let ignore_if_exists = op.options.as_ref().map_or(false, |options| { + !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(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 = op + .options + .as_ref() + .and_then(|options| options.recursive) + .unwrap_or(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 = op.options.as_ref().map_or(false, |options| { + !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) + }); + if ignore_if_exists && to.exists() { + Ok(()) + } else { + fs::rename(&from, &to) + } + } + } +} + +pub 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| { + 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(¤t_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); + 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 + // for (url, edits) in changes.iter() { + // let file_path = url.origin().ascii_serialization(); + // let file_path = std::path::PathBuf::from(file_path); + // let file = std::fs::File::open(file_path).unwrap(); + // let mut text = Rope::from_reader(file).unwrap(); + // let transaction = edits_to_changes(&text, edits); + // transaction.apply(&mut text); + // } + } + + if let Some(ref document_changes) = workspace_edit.document_changes { + match document_changes { + lsp::DocumentChanges::Edits(document_edits) => { + for document_edit in document_edits { + 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); + } + } + lsp::DocumentChanges::Operations(operations) => { + log::debug!("document changes - operations: {:?}", operations); + 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); + } + } + } + } + } + } +} +fn goto_impl( + editor: &mut Editor, + compositor: &mut Compositor, + locations: Vec, + offset_encoding: OffsetEncoding, +) { + push_jump(editor); + + // TODO: share with symbol picker(symbol.location) + fn jump_to( + editor: &mut Editor, + location: &lsp::Location, + offset_encoding: OffsetEncoding, + action: Action, + ) { + let path = location + .uri + .to_file_path() + .expect("unable to convert URI to filepath"); + let _id = editor.open(path, action).expect("editor.open failed"); + let (view, doc) = current!(editor); + let definition_pos = location.range.start; + // TODO: convert inside server + let new_pos = + if let Some(new_pos) = lsp_pos_to_pos(doc.text(), definition_pos, offset_encoding) { + new_pos + } else { + return; + }; + doc.set_selection(view.id, Selection::point(new_pos)); + align_view(doc, view, Align::Center); + } + + let cwdir = std::env::current_dir().expect("couldn't determine current directory"); + + match locations.as_slice() { + [location] => { + jump_to(editor, location, offset_encoding, Action::Replace); + } + [] => { + editor.set_error("No definition found."); + } + _locations => { + let picker = FilePicker::new( + locations, + move |location| { + let file: Cow<'_, str> = (location.uri.scheme() == "file") + .then(|| { + location + .uri + .to_file_path() + .map(|path| { + // strip root prefix + path.strip_prefix(&cwdir) + .map(|path| path.to_path_buf()) + .unwrap_or(path) + }) + .map(|path| Cow::from(path.to_string_lossy().into_owned())) + .ok() + }) + .flatten() + .unwrap_or_else(|| location.uri.as_str().into()); + let line = location.range.start.line; + format!("{}:{}", file, line).into() + }, + move |cx, location, action| jump_to(cx.editor, location, offset_encoding, action), + |_editor, location| { + // TODO: share code for symbol.location and location + let path = location.uri.to_file_path().unwrap(); + let line = Some(( + location.range.start.line as usize, + location.range.end.line as usize, + )); + Some((path, line)) + }, + ); + compositor.push(Box::new(overlayed(picker))); + } + } +} + +pub fn goto_definition(cx: &mut Context) { + 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 future = language_server.goto_definition(doc.identifier(), pos, None); + + cx.callback( + future, + move |editor: &mut Editor, + compositor: &mut Compositor, + response: Option| { + let items = match response { + Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location], + Some(lsp::GotoDefinitionResponse::Array(locations)) => locations, + Some(lsp::GotoDefinitionResponse::Link(locations)) => locations + .into_iter() + .map(|location_link| lsp::Location { + uri: location_link.target_uri, + range: location_link.target_range, + }) + .collect(), + None => Vec::new(), + }; + + goto_impl(editor, compositor, items, offset_encoding); + }, + ); +} + +pub fn goto_type_definition(cx: &mut Context) { + 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 future = language_server.goto_type_definition(doc.identifier(), pos, None); + + cx.callback( + future, + move |editor: &mut Editor, + compositor: &mut Compositor, + response: Option| { + let items = match response { + Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location], + Some(lsp::GotoDefinitionResponse::Array(locations)) => locations, + Some(lsp::GotoDefinitionResponse::Link(locations)) => locations + .into_iter() + .map(|location_link| lsp::Location { + uri: location_link.target_uri, + range: location_link.target_range, + }) + .collect(), + None => Vec::new(), + }; + + goto_impl(editor, compositor, items, offset_encoding); + }, + ); +} + +pub fn goto_implementation(cx: &mut Context) { + 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 future = language_server.goto_implementation(doc.identifier(), pos, None); + + cx.callback( + future, + move |editor: &mut Editor, + compositor: &mut Compositor, + response: Option| { + let items = match response { + Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location], + Some(lsp::GotoDefinitionResponse::Array(locations)) => locations, + Some(lsp::GotoDefinitionResponse::Link(locations)) => locations + .into_iter() + .map(|location_link| lsp::Location { + uri: location_link.target_uri, + range: location_link.target_range, + }) + .collect(), + None => Vec::new(), + }; + + goto_impl(editor, compositor, items, offset_encoding); + }, + ); +} + +pub fn goto_reference(cx: &mut Context) { + 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 future = language_server.goto_reference(doc.identifier(), pos, None); + + cx.callback( + future, + move |editor: &mut Editor, + compositor: &mut Compositor, + items: Option>| { + goto_impl( + editor, + compositor, + items.unwrap_or_default(), + offset_encoding, + ); + }, + ); +} + +pub fn signature_help(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + + let language_server = match doc.language_server() { + Some(language_server) => language_server, + None => return, + }; + + let pos = pos_to_lsp_pos( + doc.text(), + doc.selection(view.id) + .primary() + .cursor(doc.text().slice(..)), + language_server.offset_encoding(), + ); + + let future = language_server.text_document_signature_help(doc.identifier(), pos, None); + + cx.callback( + future, + move |_editor: &mut Editor, + _compositor: &mut Compositor, + response: Option| { + if let Some(signature_help) = response { + log::info!("{:?}", signature_help); + // signatures + // active_signature + // active_parameter + // render as: + + // signature + // ---------- + // doc + + // with active param highlighted + } + }, + ); +} +pub fn hover(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + + let language_server = match doc.language_server() { + Some(language_server) => language_server, + None => return, + }; + + // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier + + let pos = pos_to_lsp_pos( + doc.text(), + doc.selection(view.id) + .primary() + .cursor(doc.text().slice(..)), + language_server.offset_encoding(), + ); + + let future = language_server.text_document_hover(doc.identifier(), pos, None); + + cx.callback( + future, + move |editor: &mut Editor, compositor: &mut Compositor, response: Option| { + if let Some(hover) = response { + // hover.contents / .range <- used for visualizing + + 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) + } + } + } + } + + 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::>() + .join("\n\n"), + lsp::HoverContents::Markup(contents) => contents.value, + }; + + // skip if contents empty + + let contents = + ui::Markdown::new(contents, editor.syn_loader.clone()).style_group("hover"); + let popup = Popup::new("hover", contents); + compositor.replace_or_push("hover", Box::new(popup)); + } + }, + ); +} +pub fn rename_symbol(cx: &mut Context) { + let prompt = Prompt::new( + "rename-to:".into(), + None, + ui::completers::none, + 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(cx.editor, offset_encoding, &edits); + }, + ); + cx.push_layer(Box::new(prompt)); +} From c06155ace4ef4aa65b680093da920bded320b8f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 18 Feb 2022 14:01:50 +0900 Subject: [PATCH 012/186] Extract a helper function for lsp::Location --- helix-term/src/commands/lsp.rs | 30 ++++++++++++------------------ helix-term/src/ui/mod.rs | 2 +- helix-term/src/ui/picker.rs | 2 +- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 084c7c6a8..722490b26 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -11,11 +11,20 @@ use helix_view::editor::Action; use crate::{ compositor::{self, Compositor}, - ui::{self, overlay::overlayed, FilePicker, Popup, Prompt, PromptEvent}, + ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, Prompt, PromptEvent}, }; use std::borrow::Cow; +fn location_to_file_location(location: &lsp::Location) -> FileLocation { + let path = location.uri.to_file_path().unwrap(); + let line = Some(( + location.range.start.line as usize, + location.range.end.line as usize, + )); + (path, line) +} + fn sym_picker( symbols: Vec, current_path: Option, @@ -55,14 +64,7 @@ fn sym_picker( 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)) - }, + move |_editor, symbol| Some(location_to_file_location(&symbol.location)), ); picker.truncate_start = false; picker @@ -465,15 +467,7 @@ fn goto_impl( format!("{}:{}", file, line).into() }, move |cx, location, action| jump_to(cx.editor, location, offset_encoding, action), - |_editor, location| { - // TODO: share code for symbol.location and location - let path = location.uri.to_file_path().unwrap(); - let line = Some(( - location.range.start.line as usize, - location.range.end.line as usize, - )); - Some((path, line)) - }, + move |_editor, location| Some(location_to_file_location(location)), ); compositor.push(Box::new(overlayed(picker))); } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index e269c8edd..21c1f7aa8 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -14,7 +14,7 @@ pub use completion::Completion; pub use editor::EditorView; pub use markdown::Markdown; pub use menu::Menu; -pub use picker::{FilePicker, Picker}; +pub use picker::{FileLocation, FilePicker, Picker}; pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use spinner::{ProgressSpinners, Spinner}; diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 622af3876..9e236510f 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -33,7 +33,7 @@ pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72; pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024; /// File path and range of lines (used to align and highlight lines) -type FileLocation = (PathBuf, Option<(usize, usize)>); +pub type FileLocation = (PathBuf, Option<(usize, usize)>); pub struct FilePicker { picker: Picker, From 4e845409b6d62a87f4b552213ee931a1716c147e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 18 Feb 2022 14:05:31 +0900 Subject: [PATCH 013/186] Extract a common "language server or return" macro --- helix-term/src/commands/lsp.rs | 75 ++++++++++------------------------ 1 file changed, 21 insertions(+), 54 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 722490b26..e1fb4cbbe 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -16,6 +16,16 @@ use crate::{ use std::borrow::Cow; +#[macro_export] +macro_rules! language_server { + ($doc:expr) => { + match $doc.language_server() { + Some(language_server) => language_server, + None => return, + } + }; +} + fn location_to_file_location(location: &lsp::Location) -> FileLocation { let path = location.uri.to_file_path().unwrap(); let line = Some(( @@ -91,10 +101,7 @@ pub fn symbol_picker(cx: &mut Context) { } let doc = doc!(cx.editor); - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; + let language_server = language_server!(doc); let current_url = doc.url(); let offset_encoding = language_server.offset_encoding(); @@ -130,10 +137,7 @@ pub fn symbol_picker(cx: &mut Context) { pub fn workspace_symbol_picker(cx: &mut Context) { let doc = doc!(cx.editor); let current_url = doc.url(); - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; + let language_server = language_server!(doc); let offset_encoding = language_server.offset_encoding(); let future = language_server.workspace_symbols("".to_string()); @@ -162,10 +166,7 @@ impl ui::menu::Item for lsp::CodeActionOrCommand { pub fn code_action(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; + let language_server = language_server!(doc); let range = range_to_lsp_range( doc.text(), @@ -230,10 +231,7 @@ 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, - }; + let language_server = language_server!(doc); // the command is executed on the server and communicated back // to the client asynchronously using workspace edits @@ -476,11 +474,7 @@ fn goto_impl( pub fn goto_definition(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; - + let language_server = language_server!(doc); let offset_encoding = language_server.offset_encoding(); let pos = pos_to_lsp_pos( @@ -518,11 +512,7 @@ pub fn goto_definition(cx: &mut Context) { pub fn goto_type_definition(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; - + let language_server = language_server!(doc); let offset_encoding = language_server.offset_encoding(); let pos = pos_to_lsp_pos( @@ -560,11 +550,7 @@ pub fn goto_type_definition(cx: &mut Context) { pub fn goto_implementation(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; - + let language_server = language_server!(doc); let offset_encoding = language_server.offset_encoding(); let pos = pos_to_lsp_pos( @@ -602,11 +588,7 @@ pub fn goto_implementation(cx: &mut Context) { pub fn goto_reference(cx: &mut Context) { let (view, doc) = current!(cx.editor); - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; - + let language_server = language_server!(doc); let offset_encoding = language_server.offset_encoding(); let pos = pos_to_lsp_pos( @@ -636,11 +618,7 @@ pub fn goto_reference(cx: &mut Context) { pub fn signature_help(cx: &mut Context) { let (view, doc) = current!(cx.editor); - - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; + let language_server = language_server!(doc); let pos = pos_to_lsp_pos( doc.text(), @@ -675,11 +653,7 @@ pub fn signature_help(cx: &mut Context) { } pub fn hover(cx: &mut Context) { let (view, doc) = current!(cx.editor); - - let language_server = match doc.language_server() { - Some(language_server) => language_server, - None => return, - }; + let language_server = language_server!(doc); // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier @@ -742,14 +716,8 @@ pub fn rename_symbol(cx: &mut Context) { 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 language_server = language_server!(doc); let offset_encoding = language_server.offset_encoding(); let pos = pos_to_lsp_pos( @@ -762,7 +730,6 @@ pub fn rename_symbol(cx: &mut Context) { let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string()); let edits = block_on(task).unwrap_or_default(); - log::debug!("Edits from LSP: {:?}", edits); apply_workspace_edit(cx.editor, offset_encoding, &edits); }, ); From 1cd710fe01eb2ccb1d35d25e74d967b5645e2ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 18 Feb 2022 14:07:35 +0900 Subject: [PATCH 014/186] Extract jump_to_location --- helix-term/src/commands/lsp.rs | 57 ++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index e1fb4cbbe..eee6968e2 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -35,6 +35,32 @@ fn location_to_file_location(location: &lsp::Location) -> FileLocation { (path, line) } +// TODO: share with symbol picker(symbol.location) +// TODO: need to use push_jump() before? +fn jump_to_location( + editor: &mut Editor, + location: &lsp::Location, + offset_encoding: OffsetEncoding, + action: Action, +) { + let path = location + .uri + .to_file_path() + .expect("unable to convert URI to filepath"); + let _id = editor.open(path, action).expect("editor.open failed"); + let (view, doc) = current!(editor); + let definition_pos = location.range.start; + // TODO: convert inside server + let new_pos = if let Some(new_pos) = lsp_pos_to_pos(doc.text(), definition_pos, offset_encoding) + { + new_pos + } else { + return; + }; + doc.set_selection(view.id, Selection::point(new_pos)); + align_view(doc, view, Align::Center); +} + fn sym_picker( symbols: Vec, current_path: Option, @@ -407,36 +433,11 @@ fn goto_impl( ) { push_jump(editor); - // TODO: share with symbol picker(symbol.location) - fn jump_to( - editor: &mut Editor, - location: &lsp::Location, - offset_encoding: OffsetEncoding, - action: Action, - ) { - let path = location - .uri - .to_file_path() - .expect("unable to convert URI to filepath"); - let _id = editor.open(path, action).expect("editor.open failed"); - let (view, doc) = current!(editor); - let definition_pos = location.range.start; - // TODO: convert inside server - let new_pos = - if let Some(new_pos) = lsp_pos_to_pos(doc.text(), definition_pos, offset_encoding) { - new_pos - } else { - return; - }; - doc.set_selection(view.id, Selection::point(new_pos)); - align_view(doc, view, Align::Center); - } - let cwdir = std::env::current_dir().expect("couldn't determine current directory"); match locations.as_slice() { [location] => { - jump_to(editor, location, offset_encoding, Action::Replace); + jump_to_location(editor, location, offset_encoding, Action::Replace); } [] => { editor.set_error("No definition found."); @@ -464,7 +465,9 @@ fn goto_impl( let line = location.range.start.line; format!("{}:{}", file, line).into() }, - move |cx, location, action| jump_to(cx.editor, location, offset_encoding, action), + move |cx, location, action| { + jump_to_location(cx.editor, location, offset_encoding, action) + }, move |_editor, location| Some(location_to_file_location(location)), ); compositor.push(Box::new(overlayed(picker))); From 5af9136aec31f712e2dea0f6545edb6fd3bdf4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 18 Feb 2022 14:11:50 +0900 Subject: [PATCH 015/186] Extract some duplication in lsp goto_ calls --- helix-term/src/commands/lsp.rs | 85 ++++++++++------------------------ 1 file changed, 25 insertions(+), 60 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index eee6968e2..7bbcc60af 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -475,6 +475,21 @@ fn goto_impl( } } +fn to_locations(definitions: Option) -> Vec { + match definitions { + Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location], + Some(lsp::GotoDefinitionResponse::Array(locations)) => locations, + Some(lsp::GotoDefinitionResponse::Link(locations)) => locations + .into_iter() + .map(|location_link| lsp::Location { + uri: location_link.target_uri, + range: location_link.target_range, + }) + .collect(), + None => Vec::new(), + } +} + pub fn goto_definition(cx: &mut Context) { let (view, doc) = current!(cx.editor); let language_server = language_server!(doc); @@ -492,22 +507,8 @@ pub fn goto_definition(cx: &mut Context) { cx.callback( future, - move |editor: &mut Editor, - compositor: &mut Compositor, - response: Option| { - let items = match response { - Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location], - Some(lsp::GotoDefinitionResponse::Array(locations)) => locations, - Some(lsp::GotoDefinitionResponse::Link(locations)) => locations - .into_iter() - .map(|location_link| lsp::Location { - uri: location_link.target_uri, - range: location_link.target_range, - }) - .collect(), - None => Vec::new(), - }; - + move |editor, compositor, response: Option| { + let items = to_locations(response); goto_impl(editor, compositor, items, offset_encoding); }, ); @@ -530,22 +531,8 @@ pub fn goto_type_definition(cx: &mut Context) { cx.callback( future, - move |editor: &mut Editor, - compositor: &mut Compositor, - response: Option| { - let items = match response { - Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location], - Some(lsp::GotoDefinitionResponse::Array(locations)) => locations, - Some(lsp::GotoDefinitionResponse::Link(locations)) => locations - .into_iter() - .map(|location_link| lsp::Location { - uri: location_link.target_uri, - range: location_link.target_range, - }) - .collect(), - None => Vec::new(), - }; - + move |editor, compositor, response: Option| { + let items = to_locations(response); goto_impl(editor, compositor, items, offset_encoding); }, ); @@ -568,22 +555,8 @@ pub fn goto_implementation(cx: &mut Context) { cx.callback( future, - move |editor: &mut Editor, - compositor: &mut Compositor, - response: Option| { - let items = match response { - Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location], - Some(lsp::GotoDefinitionResponse::Array(locations)) => locations, - Some(lsp::GotoDefinitionResponse::Link(locations)) => locations - .into_iter() - .map(|location_link| lsp::Location { - uri: location_link.target_uri, - range: location_link.target_range, - }) - .collect(), - None => Vec::new(), - }; - + move |editor, compositor, response: Option| { + let items = to_locations(response); goto_impl(editor, compositor, items, offset_encoding); }, ); @@ -606,15 +579,9 @@ pub fn goto_reference(cx: &mut Context) { cx.callback( future, - move |editor: &mut Editor, - compositor: &mut Compositor, - items: Option>| { - goto_impl( - editor, - compositor, - items.unwrap_or_default(), - offset_encoding, - ); + move |editor, compositor, response: Option>| { + let items = response.unwrap_or_default(); + goto_impl(editor, compositor, items, offset_encoding); }, ); } @@ -635,9 +602,7 @@ pub fn signature_help(cx: &mut Context) { cx.callback( future, - move |_editor: &mut Editor, - _compositor: &mut Compositor, - response: Option| { + move |_editor, _compositor, response: Option| { if let Some(signature_help) = response { log::info!("{:?}", signature_help); // signatures From a449156702112a1ee1d11ef2f5495067d801deef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 18 Feb 2022 14:33:56 +0900 Subject: [PATCH 016/186] Extract a lsp position helper --- helix-term/src/commands/lsp.rs | 60 ++++++---------------------------- helix-view/src/document.rs | 14 ++++++++ 2 files changed, 24 insertions(+), 50 deletions(-) diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 7bbcc60af..255d545c7 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1,6 +1,6 @@ use helix_lsp::{ block_on, lsp, - util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range}, + util::{lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range}, OffsetEncoding, }; @@ -495,13 +495,7 @@ pub fn goto_definition(cx: &mut Context) { let language_server = language_server!(doc); 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 pos = doc.position(view.id, offset_encoding); let future = language_server.goto_definition(doc.identifier(), pos, None); @@ -519,13 +513,7 @@ pub fn goto_type_definition(cx: &mut Context) { let language_server = language_server!(doc); 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 pos = doc.position(view.id, offset_encoding); let future = language_server.goto_type_definition(doc.identifier(), pos, None); @@ -543,13 +531,7 @@ pub fn goto_implementation(cx: &mut Context) { let language_server = language_server!(doc); 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 pos = doc.position(view.id, offset_encoding); let future = language_server.goto_implementation(doc.identifier(), pos, None); @@ -567,13 +549,7 @@ pub fn goto_reference(cx: &mut Context) { let language_server = language_server!(doc); 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 pos = doc.position(view.id, offset_encoding); let future = language_server.goto_reference(doc.identifier(), pos, None); @@ -589,14 +565,9 @@ pub fn goto_reference(cx: &mut Context) { pub fn signature_help(cx: &mut Context) { let (view, doc) = current!(cx.editor); let language_server = language_server!(doc); + let offset_encoding = language_server.offset_encoding(); - let pos = pos_to_lsp_pos( - doc.text(), - doc.selection(view.id) - .primary() - .cursor(doc.text().slice(..)), - language_server.offset_encoding(), - ); + let pos = doc.position(view.id, offset_encoding); let future = language_server.text_document_signature_help(doc.identifier(), pos, None); @@ -622,16 +593,11 @@ pub fn signature_help(cx: &mut Context) { pub fn hover(cx: &mut Context) { let (view, doc) = current!(cx.editor); let language_server = language_server!(doc); + let offset_encoding = language_server.offset_encoding(); // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier - let pos = pos_to_lsp_pos( - doc.text(), - doc.selection(view.id) - .primary() - .cursor(doc.text().slice(..)), - language_server.offset_encoding(), - ); + let pos = doc.position(view.id, offset_encoding); let future = language_server.text_document_hover(doc.identifier(), pos, None); @@ -688,13 +654,7 @@ pub fn rename_symbol(cx: &mut Context) { let language_server = language_server!(doc); 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 pos = doc.position(view.id, offset_encoding); let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string()); let edits = block_on(task).unwrap_or_default(); diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index c0186ee53..f13338ba4 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -922,6 +922,20 @@ impl Document { lsp::VersionedTextDocumentIdentifier::new(self.url().unwrap(), self.version) } + pub fn position( + &self, + view_id: ViewId, + offset_encoding: helix_lsp::OffsetEncoding, + ) -> lsp::Position { + let text = self.text(); + + helix_lsp::util::pos_to_lsp_pos( + text, + self.selection(view_id).primary().cursor(text.slice(..)), + offset_encoding, + ) + } + #[inline] pub fn diagnostics(&self) -> &[Diagnostic] { &self.diagnostics From 2af04325d83cd0141400951252574666cffdf1af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 20 Feb 2022 14:44:44 +0900 Subject: [PATCH 017/186] fix: Allow multi-line prompt documentation --- helix-term/src/ui/markdown.rs | 15 ++------------- helix-term/src/ui/prompt.rs | 16 +++++++++++----- helix-term/src/ui/text.rs | 18 +++++++++++++++++- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 6a7b641ad..cfb5998c6 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -247,19 +247,8 @@ impl Component for Markdown { // TODO: account for tab width let max_text_width = (viewport.0 - padding).min(120); - let mut text_width = 0; - let mut height = padding; - for content in contents { - height += 1; - let content_width = content.width() as u16; - if content_width > max_text_width { - text_width = max_text_width; - height += content_width / max_text_width; - } else if content_width > text_width { - text_width = content_width; - } - } + let (width, height) = crate::ui::text::required_size(&contents, max_text_width); - Some((text_width + padding, height)) + Some((width + padding * 2, height)) } } diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 18b390dd1..7088d6dfe 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -389,12 +389,18 @@ impl Prompt { if let Some(doc) = (self.doc_fn)(&self.line) { let mut text = ui::Text::new(doc.to_string()); + let max_width = BASE_WIDTH * 3; + let padding = 1; + let viewport = area; + + let (_width, height) = ui::text::required_size(&text.contents, max_width); + let area = viewport.intersection(Rect::new( completion_area.x, - completion_area.y.saturating_sub(3), - BASE_WIDTH * 3, - 3, + completion_area.y.saturating_sub(height + padding * 2), + max_width, + height + padding * 2, )); let background = theme.get("ui.help"); @@ -402,8 +408,8 @@ impl Prompt { text.render( area.inner(&Margin { - vertical: 1, - horizontal: 1, + vertical: padding, + horizontal: padding, }), surface, cx, diff --git a/helix-term/src/ui/text.rs b/helix-term/src/ui/text.rs index caece049c..c318052b2 100644 --- a/helix-term/src/ui/text.rs +++ b/helix-term/src/ui/text.rs @@ -4,7 +4,7 @@ use tui::buffer::Buffer as Surface; use helix_view::graphics::Rect; pub struct Text { - contents: tui::text::Text<'static>, + pub(crate) contents: tui::text::Text<'static>, size: (u16, u16), viewport: (u16, u16), } @@ -49,3 +49,19 @@ impl Component for Text { Some(self.size) } } + +pub fn required_size(text: &tui::text::Text, max_text_width: u16) -> (u16, u16) { + let mut text_width = 0; + let mut height = 0; + for content in &text.lines { + height += 1; + let content_width = content.width() as u16; + if content_width > max_text_width { + text_width = max_text_width; + height += content_width / max_text_width; + } else if content_width > text_width { + text_width = content_width; + } + } + (text_width, height) +} From c7b326be047a42f6a58146b0de049d48568e397f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 20 Feb 2022 14:55:16 +0900 Subject: [PATCH 018/186] ui: prompt: Render aliases + border on the doc --- helix-term/src/commands.rs | 7 +++++-- helix-term/src/ui/prompt.rs | 23 ++++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7e8b1d53e..982ae0135 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3384,8 +3384,11 @@ fn command_mode(cx: &mut Context) { prompt.doc_fn = Box::new(|input: &str| { let part = input.split(' ').next().unwrap_or_default(); - if let Some(cmd::TypableCommand { doc, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) { - return Some(doc); + if let Some(cmd::TypableCommand { doc, aliases, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) { + if aliases.is_empty() { + return Some((*doc).into()); + } + return Some(format!("{}\nAliases: {}", doc, aliases.join(", ")).into()); } None diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 7088d6dfe..cd8e14eea 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -5,6 +5,7 @@ use helix_view::input::KeyEvent; use helix_view::keyboard::{KeyCode, KeyModifiers}; use std::{borrow::Cow, ops::RangeFrom}; use tui::buffer::Buffer as Surface; +use tui::widgets::{Block, Borders, Widget}; use helix_core::{ unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position, @@ -26,7 +27,7 @@ pub struct Prompt { history_pos: Option, completion_fn: Box Vec>, callback_fn: Box, - pub doc_fn: Box Option<&'static str>>, + pub doc_fn: Box Option>>, } #[derive(Clone, Copy, PartialEq)] @@ -406,14 +407,18 @@ impl Prompt { let background = theme.get("ui.help"); surface.clear_with(area, background); - text.render( - area.inner(&Margin { - vertical: padding, - horizontal: padding, - }), - surface, - cx, - ); + let block = Block::default() + // .title(self.title.as_str()) + .borders(Borders::ALL) + .border_style(background); + + let inner = block.inner(area).inner(&Margin { + vertical: 0, + horizontal: 1, + }); + + block.render(area, surface); + text.render(inner, surface, cx); } let line = area.height - 1; From 700058f433d0d12b30b968d50cd7f9b93f983107 Mon Sep 17 00:00:00 2001 From: Daniel S Poulin Date: Sun, 20 Feb 2022 01:47:43 -0500 Subject: [PATCH 019/186] Always ignore the .git directory in file picker (#1604) Some users (including myself) want to turn off filtering of files prefixed with `.`, as they are often useful to edit. For example, `.env` files, configuration for linters `.eslint.json` and the like. --- helix-term/src/ui/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 21c1f7aa8..94b930a01 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -111,7 +111,11 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi .git_ignore(config.file_picker.git_ignore) .git_global(config.file_picker.git_global) .git_exclude(config.file_picker.git_exclude) - .max_depth(config.file_picker.max_depth); + .max_depth(config.file_picker.max_depth) + // We always want to ignore the .git directory, otherwise if + // `ignore` is turned off above, we end up with a lot of noise + // in our picker. + .filter_entry(|entry| entry.file_name() != ".git"); let walk_builder = match type_builder.add( "compressed", From d5ba0b516263f08116c43eb0796b1027b094dabd Mon Sep 17 00:00:00 2001 From: Alex <3957610+CptPotato@users.noreply.github.com> Date: Mon, 21 Feb 2022 08:45:48 +0100 Subject: [PATCH 020/186] Allow separate styles for markup headings (#1618) * update markdown highlighting to use separate heading themes * remove markdown theme scopes in ui --- book/src/themes.md | 2 + helix-term/src/commands/lsp.rs | 3 +- helix-term/src/ui/completion.rs | 8 ++-- helix-term/src/ui/markdown.rs | 62 ++++++++++++++----------- runtime/queries/markdown/highlights.scm | 13 ++++-- 5 files changed, 51 insertions(+), 37 deletions(-) diff --git a/book/src/themes.md b/book/src/themes.md index 9abcfe8c1..78b4d14bd 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -166,6 +166,8 @@ We use a similar set of scopes as - `markup` - `heading` + - `marker` + - `1`, `2`, `3`, `4`, `5`, `6` - heading text for h1 through h6 - `list` - `unnumbered` - `numbered` diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 255d545c7..af11cd4e9 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -632,8 +632,7 @@ pub fn hover(cx: &mut Context) { // skip if contents empty - let contents = - ui::Markdown::new(contents, editor.syn_loader.clone()).style_group("hover"); + let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); let popup = Popup::new("hover", contents); compositor.replace_or_push("hover", Box::new(popup)); } diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 35afe81e9..d3b618a91 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -305,8 +305,6 @@ impl Component for Completion { let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width()); let cursor_pos = (coords.row - view.offset.row) as u16; - let markdown_ui = - |content, syn_loader| Markdown::new(content, syn_loader).style_group("completion"); let mut markdown_doc = match &option.documentation { Some(lsp::Documentation::String(contents)) | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { @@ -314,7 +312,7 @@ impl Component for Completion { value: contents, })) => { // TODO: convert to wrapped text - markdown_ui( + Markdown::new( format!( "```{}\n{}\n```\n{}", language, @@ -329,7 +327,7 @@ impl Component for Completion { value: contents, })) => { // TODO: set language based on doc scope - markdown_ui( + Markdown::new( format!( "```{}\n{}\n```\n{}", language, @@ -343,7 +341,7 @@ impl Component for Completion { // TODO: copied from above // TODO: set language based on doc scope - markdown_ui( + Markdown::new( format!( "```{}\n{}\n```", language, diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index cfb5998c6..bcb63c29a 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -6,14 +6,14 @@ use tui::{ use std::sync::Arc; -use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag}; +use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag}; use helix_core::{ syntax::{self, HighlightEvent, Syntax}, Rope, }; use helix_view::{ - graphics::{Margin, Rect}, + graphics::{Margin, Rect, Style}, Theme, }; @@ -21,30 +21,31 @@ pub struct Markdown { contents: String, config_loader: Arc, - - block_style: String, - heading_style: String, } // TODO: pre-render and self reference via Pin // better yet, just use Tendril + subtendril for references impl Markdown { + // theme keys, including fallbacks + const TEXT_STYLE: [&'static str; 2] = ["ui.text", "ui"]; + const BLOCK_STYLE: [&'static str; 3] = ["markup.raw.inline", "markup.raw", "markup"]; + const HEADING_STYLES: [[&'static str; 3]; 6] = [ + ["markup.heading.1", "markup.heading", "markup"], + ["markup.heading.2", "markup.heading", "markup"], + ["markup.heading.3", "markup.heading", "markup"], + ["markup.heading.4", "markup.heading", "markup"], + ["markup.heading.5", "markup.heading", "markup"], + ["markup.heading.6", "markup.heading", "markup"], + ]; + pub fn new(contents: String, config_loader: Arc) -> Self { Self { contents, config_loader, - block_style: "markup.raw.inline".into(), - heading_style: "markup.heading".into(), } } - pub fn style_group(mut self, suffix: &str) -> Self { - self.block_style = format!("markup.raw.inline.{}", suffix); - self.heading_style = format!("markup.heading.{}", suffix); - self - } - fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> { // // also 2021-03-04T16:33:58.553 helix_lsp::transport [INFO] <- {"contents":{"kind":"markdown","value":"\n```rust\ncore::num\n```\n\n```rust\npub const fn saturating_sub(self, rhs:Self) ->Self\n```\n\n---\n\n```rust\n```"},"range":{"end":{"character":61,"line":101},"start":{"character":47,"line":101}}} // let text = "\n```rust\ncore::iter::traits::iterator::Iterator\n```\n\n```rust\nfn collect>(self) -> B\nwhere\n Self: Sized,\n```\n\n---\n\nTransforms an iterator into a collection.\n\n`collect()` can take anything iterable, and turn it into a relevant\ncollection. This is one of the more powerful methods in the standard\nlibrary, used in a variety of contexts.\n\nThe most basic pattern in which `collect()` is used is to turn one\ncollection into another. You take a collection, call [`iter`](https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html) on it,\ndo a bunch of transformations, and then `collect()` at the end.\n\n`collect()` can also create instances of types that are not typical\ncollections. For example, a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html) can be built from [`char`](type@char)s,\nand an iterator of [`Result`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html) items can be collected\ninto `Result, E>`. See the examples below for more.\n\nBecause `collect()` is so general, it can cause problems with type\ninference. As such, `collect()` is one of the few times you'll see\nthe syntax affectionately known as the 'turbofish': `::<>`. This\nhelps the inference algorithm understand specifically which collection\nyou're trying to collect into.\n\n# Examples\n\nBasic usage:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled: Vec = a.iter()\n .map(|&x| x * 2)\n .collect();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nNote that we needed the `: Vec` on the left-hand side. This is because\nwe could collect into, for example, a [`VecDeque`](https://doc.rust-lang.org/nightly/core/iter/std/collections/struct.VecDeque.html) instead:\n\n```rust\nuse std::collections::VecDeque;\n\nlet a = [1, 2, 3];\n\nlet doubled: VecDeque = a.iter().map(|&x| x * 2).collect();\n\nassert_eq!(2, doubled[0]);\nassert_eq!(4, doubled[1]);\nassert_eq!(6, doubled[2]);\n```\n\nUsing the 'turbofish' instead of annotating `doubled`:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nBecause `collect()` only cares about what you're collecting into, you can\nstill use a partial type hint, `_`, with the turbofish:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nUsing `collect()` to make a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html):\n\n```rust\nlet chars = ['g', 'd', 'k', 'k', 'n'];\n\nlet hello: String = chars.iter()\n .map(|&x| x as u8)\n .map(|x| (x + 1) as char)\n .collect();\n\nassert_eq!(\"hello\", hello);\n```\n\nIf you have a list of [`Result`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html)s, you can use `collect()` to\nsee if any of them failed:\n\n```rust\nlet results = [Ok(1), Err(\"nope\"), Ok(3), Err(\"bad\")];\n\nlet result: Result, &str> = results.iter().cloned().collect();\n\n// gives us the first error\nassert_eq!(Err(\"nope\"), result);\n\nlet results = [Ok(1), Ok(3)];\n\nlet result: Result, &str> = results.iter().cloned().collect();\n\n// gives us the list of answers\nassert_eq!(Ok(vec![1, 3]), result);\n```"; @@ -67,17 +68,19 @@ impl Markdown { }) } - macro_rules! get_theme { - ($s1: expr) => { - theme - .map(|theme| theme.try_get($s1.as_str())) - .flatten() - .unwrap_or_default() - }; - } - let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default(); - let code_style = get_theme!(self.block_style); - let heading_style = get_theme!(self.heading_style); + let get_theme = |keys: &[&str]| match theme { + Some(theme) => keys + .iter() + .find_map(|key| theme.try_get(key)) + .unwrap_or_default(), + None => Default::default(), + }; + let text_style = get_theme(&Self::TEXT_STYLE); + let code_style = get_theme(&Self::BLOCK_STYLE); + let heading_styles: Vec