From b26e7e2e8fc900b5637d9772ecb74874e8794ecc Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 5 Jul 2022 06:44:16 -0400 Subject: [PATCH] Add live preview to theme picker (#1798) * Add theme picker with live preview * Add live theme preview to :theme command * cargo fmt * Fix clippy warnings * Remove picker variant * Remove unused import * Cleanup * Change current_theme to last_theme * Fix accidental comment flash deletion * Typo * Remove theme cache * Add some comments * Refactor some theme handling TIL flatmap on Option is called and_then * Remove unnecessary renames * Constrain last_theme theme preview lifecycle * Switch to bitflag implementation * Better handling of last_theme * Sort theme names * Better memory juggling * Missed a branch * Remove name from theme, switch bitand to & * cargo fmt * Update helix-view/src/editor.rs * Switch boolean to enum * Remove bitflag impl * cargo fmt * Remove un-needed type arg * cargo fmt --- helix-term/src/application.rs | 16 +- helix-term/src/commands/typed.rs | 472 ++++++++++++++++++++++++------- helix-term/src/ui/mod.rs | 1 + helix-term/src/ui/picker.rs | 2 +- helix-view/src/editor.rs | 41 ++- helix-view/src/theme.rs | 8 + 6 files changed, 422 insertions(+), 118 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 805f660f6..df14f5e3d 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -108,13 +108,7 @@ impl Application { .ok() .filter(|theme| (true_color || theme.is_16_color())) }) - .unwrap_or_else(|| { - if true_color { - theme_loader.default() - } else { - theme_loader.base16_default() - } - }); + .unwrap_or_else(|| theme_loader.default_theme(true_color)); let syn_loader_conf = user_syntax_loader().unwrap_or_else(|err| { eprintln!("Bad language config: {}", err); @@ -373,13 +367,7 @@ impl Application { }) .ok() .filter(|theme| (true_color || theme.is_16_color())) - .unwrap_or_else(|| { - if true_color { - self.theme_loader.default() - } else { - self.theme_loader.base16_default() - } - }), + .unwrap_or_else(|| self.theme_loader.default_theme(true_color)), ); } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 70f5fa9fe..4e1ac0da9 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -15,11 +15,11 @@ pub struct TypableCommand { pub completer: Option, } -fn quit( - cx: &mut compositor::Context, - args: &[Cow], - _event: PromptEvent, -) -> anyhow::Result<()> { +fn quit(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + ensure!(args.is_empty(), ":quit takes no arguments"); // last view and we have unsaved changes @@ -35,8 +35,12 @@ fn quit( fn force_quit( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + ensure!(args.is_empty(), ":quit! takes no arguments"); cx.editor.close(view!(cx.editor).id); @@ -44,11 +48,11 @@ fn force_quit( Ok(()) } -fn open( - cx: &mut compositor::Context, - args: &[Cow], - _event: PromptEvent, -) -> anyhow::Result<()> { +fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + ensure!(!args.is_empty(), "wrong argument count"); for arg in args { let (path, pos) = args::parse_file(arg); @@ -114,8 +118,12 @@ fn buffer_gather_paths_impl(editor: &mut Editor, args: &[Cow]) -> Vec], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let document_ids = buffer_gather_paths_impl(cx.editor, args); buffer_close_by_ids_impl(cx.editor, &document_ids, false) } @@ -123,8 +131,12 @@ fn buffer_close( fn force_buffer_close( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let document_ids = buffer_gather_paths_impl(cx.editor, args); buffer_close_by_ids_impl(cx.editor, &document_ids, true) } @@ -141,8 +153,12 @@ fn buffer_gather_others_impl(editor: &mut Editor) -> Vec { fn buffer_close_others( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let document_ids = buffer_gather_others_impl(cx.editor); buffer_close_by_ids_impl(cx.editor, &document_ids, false) } @@ -150,8 +166,12 @@ fn buffer_close_others( fn force_buffer_close_others( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let document_ids = buffer_gather_others_impl(cx.editor); buffer_close_by_ids_impl(cx.editor, &document_ids, true) } @@ -163,8 +183,12 @@ fn buffer_gather_all_impl(editor: &mut Editor) -> Vec { fn buffer_close_all( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let document_ids = buffer_gather_all_impl(cx.editor); buffer_close_by_ids_impl(cx.editor, &document_ids, false) } @@ -172,8 +196,12 @@ fn buffer_close_all( fn force_buffer_close_all( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let document_ids = buffer_gather_all_impl(cx.editor); buffer_close_by_ids_impl(cx.editor, &document_ids, true) } @@ -181,8 +209,12 @@ fn force_buffer_close_all( fn buffer_next( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + goto_buffer(cx.editor, Direction::Forward); Ok(()) } @@ -190,8 +222,12 @@ fn buffer_next( fn buffer_previous( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + goto_buffer(cx.editor, Direction::Backward); Ok(()) } @@ -242,24 +278,36 @@ fn write_impl( fn write( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + write_impl(cx, args.first(), false) } fn force_write( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + write_impl(cx, args.first(), true) } fn new_file( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + cx.editor.new_file(Action::Replace); Ok(()) @@ -268,8 +316,12 @@ fn new_file( fn format( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let doc = doc!(cx.editor); if let Some(format) = doc.format() { let callback = @@ -282,8 +334,12 @@ fn format( fn set_indent_style( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + use IndentStyle::*; // If no argument, report current indent style. @@ -321,8 +377,12 @@ fn set_indent_style( fn set_line_ending( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + use LineEnding::*; // If no argument, report current line ending setting. @@ -391,8 +451,12 @@ fn set_line_ending( fn earlier( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); @@ -407,8 +471,12 @@ fn earlier( fn later( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); let success = doc.later(view.id, uk); @@ -424,6 +492,10 @@ fn write_quit( args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + write_impl(cx, args.first(), false)?; helix_lsp::block_on(cx.jobs.finish())?; quit(cx, &[], event) @@ -434,6 +506,10 @@ fn force_write_quit( args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + write_impl(cx, args.first(), true)?; force_quit(cx, &[], event) } @@ -463,10 +539,14 @@ pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> fn write_all_impl( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, quit: bool, force: bool, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let mut errors = String::new(); let auto_format = cx.editor.config().auto_format; let jobs = &mut cx.jobs; @@ -520,6 +600,10 @@ fn write_all( args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + write_all_impl(cx, args, event, false, false) } @@ -528,6 +612,10 @@ fn write_all_quit( args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + write_all_impl(cx, args, event, true, false) } @@ -536,6 +624,10 @@ fn force_write_all_quit( args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + write_all_impl(cx, args, event, true, true) } @@ -556,24 +648,36 @@ fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> { fn quit_all( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + quit_all_impl(cx.editor, false) } fn force_quit_all( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + quit_all_impl(cx.editor, true) } fn cquit( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let exit_code = args .first() .and_then(|code| code.parse::().ok()) @@ -586,8 +690,12 @@ fn cquit( fn force_cquit( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let exit_code = args .first() .and_then(|code| code.parse::().ok()) @@ -600,35 +708,61 @@ fn force_cquit( fn theme( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, -) -> anyhow::Result<()> { - let theme = args.first().context("Theme not provided")?; - let theme = cx - .editor - .theme_loader - .load(theme) - .with_context(|| format!("Failed setting theme {}", theme))?; - let true_color = cx.editor.config().true_color || crate::true_color(); - if !(true_color || theme.is_16_color()) { - bail!("Unsupported theme: theme requires true color support"); - } - cx.editor.set_theme(theme); + event: PromptEvent, +) -> anyhow::Result<()> { + let true_color = cx.editor.config.load().true_color || crate::true_color(); + match event { + PromptEvent::Abort => { + cx.editor.unset_theme_preview(); + } + PromptEvent::Update => { + if let Some(theme_name) = args.first() { + if let Ok(theme) = cx.editor.theme_loader.load(theme_name) { + if !(true_color || theme.is_16_color()) { + bail!("Unsupported theme: theme requires true color support"); + } + cx.editor.set_theme_preview(theme); + }; + }; + } + PromptEvent::Validate => { + let theme_name = args.first().with_context(|| "Theme name not provided")?; + let theme = cx + .editor + .theme_loader + .load(theme_name) + .with_context(|| "Theme does not exist")?; + if !(true_color || theme.is_16_color()) { + bail!("Unsupported theme: theme requires true color support"); + } + cx.editor.set_theme(theme); + } + }; + Ok(()) } fn yank_main_selection_to_clipboard( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard) } fn yank_joined_to_clipboard( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let doc = doc!(cx.editor); let default_sep = Cow::Borrowed(doc.line_ending.as_str()); let separator = args.first().unwrap_or(&default_sep); @@ -638,16 +772,24 @@ fn yank_joined_to_clipboard( fn yank_main_selection_to_primary_clipboard( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection) } fn yank_joined_to_primary_clipboard( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let doc = doc!(cx.editor); let default_sep = Cow::Borrowed(doc.line_ending.as_str()); let separator = args.first().unwrap_or(&default_sep); @@ -657,32 +799,48 @@ fn yank_joined_to_primary_clipboard( fn paste_clipboard_after( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1) } fn paste_clipboard_before( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + paste_clipboard_impl(cx.editor, Paste::Before, ClipboardType::Clipboard, 1) } fn paste_primary_clipboard_after( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1) } fn paste_primary_clipboard_before( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + paste_clipboard_impl(cx.editor, Paste::Before, ClipboardType::Selection, 1) } @@ -710,24 +868,36 @@ fn replace_selections_with_clipboard_impl( fn replace_selections_with_clipboard( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard) } fn replace_selections_with_primary_clipboard( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + replace_selections_with_clipboard_impl(cx, ClipboardType::Selection) } fn show_clipboard_provider( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + cx.editor .set_status(cx.editor.clipboard_provider.name().to_string()); Ok(()) @@ -736,8 +906,12 @@ fn show_clipboard_provider( fn change_current_directory( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let dir = helix_core::path::expand_tilde( args.first() .context("target directory not provided")? @@ -760,8 +934,12 @@ fn change_current_directory( fn show_current_directory( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let cwd = std::env::current_dir().context("Couldn't get the new working directory")?; cx.editor .set_status(format!("Current working directory is {}", cwd.display())); @@ -772,8 +950,12 @@ fn show_current_directory( fn set_encoding( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let doc = doc_mut!(cx.editor); if let Some(label) = args.first() { doc.set_encoding(label) @@ -788,8 +970,12 @@ fn set_encoding( fn reload( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let scrolloff = cx.editor.config().scrolloff; let (view, doc) = current!(cx.editor); doc.reload(view.id).map(|_| { @@ -800,8 +986,12 @@ fn reload( fn tree_sitter_scopes( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -814,8 +1004,12 @@ fn tree_sitter_scopes( fn vsplit( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let id = view!(cx.editor).doc; if args.is_empty() { @@ -833,8 +1027,12 @@ fn vsplit( fn hsplit( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let id = view!(cx.editor).doc; if args.is_empty() { @@ -852,8 +1050,12 @@ fn hsplit( fn vsplit_new( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + cx.editor.new_file(Action::VerticalSplit); Ok(()) @@ -862,8 +1064,12 @@ fn vsplit_new( fn hsplit_new( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + cx.editor.new_file(Action::HorizontalSplit); Ok(()) @@ -872,8 +1078,12 @@ fn hsplit_new( fn debug_eval( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + if let Some(debugger) = cx.editor.debugger.as_mut() { let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) { (Some(frame), Some(thread_id)) => (frame, thread_id), @@ -894,8 +1104,12 @@ fn debug_eval( fn debug_start( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let mut args = args.to_owned(); let name = match args.len() { 0 => None, @@ -907,8 +1121,12 @@ fn debug_start( fn debug_remote( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let mut args = args.to_owned(); let address = match args.len() { 0 => None, @@ -924,8 +1142,12 @@ fn debug_remote( fn tutor( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let path = helix_loader::runtime_dir().join("tutor.txt"); cx.editor.open(&path, Action::Replace)?; // Unset path to prevent accidentally saving to the original tutor file. @@ -936,8 +1158,12 @@ fn tutor( pub(super) fn goto_line_number( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + ensure!(!args.is_empty(), "Line number required"); let line = args[0].parse::()?; @@ -954,8 +1180,12 @@ pub(super) fn goto_line_number( fn get_option( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + if args.len() != 1 { anyhow::bail!("Bad arguments. Usage: `:get key`"); } @@ -976,8 +1206,12 @@ fn get_option( fn set_option( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + if args.len() != 2 { anyhow::bail!("Bad arguments. Usage: `:set key field`"); } @@ -1009,8 +1243,12 @@ fn set_option( fn language( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + if args.len() != 1 { anyhow::bail!("Bad arguments. Usage: `:set-language language`"); } @@ -1023,19 +1261,23 @@ fn language( Ok(()) } -fn sort( - cx: &mut compositor::Context, - args: &[Cow], - _event: PromptEvent, -) -> anyhow::Result<()> { +fn sort(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + sort_impl(cx, args, false) } fn sort_reverse( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + sort_impl(cx, args, true) } @@ -1076,8 +1318,12 @@ fn sort_impl( fn reflow( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let (view, doc) = current!(cx.editor); const DEFAULT_MAX_LEN: usize = 79; @@ -1115,8 +1361,12 @@ fn reflow( fn tree_sitter_subtree( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let (view, doc) = current!(cx.editor); if let Some(syntax) = doc.syntax() { @@ -1151,8 +1401,12 @@ fn tree_sitter_subtree( fn open_config( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + cx.editor .open(&helix_loader::config_file(), Action::Replace)?; Ok(()) @@ -1161,8 +1415,12 @@ fn open_config( fn open_log( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + cx.editor.open(&helix_loader::log_file(), Action::Replace)?; Ok(()) } @@ -1170,8 +1428,12 @@ fn open_log( fn refresh_config( cx: &mut compositor::Context, _args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + cx.editor.config_events.0.send(ConfigEvent::Refresh)?; Ok(()) } @@ -1179,8 +1441,12 @@ fn refresh_config( fn append_output( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + ensure!(!args.is_empty(), "Shell command required"); shell(cx, &args.join(" "), &ShellBehavior::Append); Ok(()) @@ -1189,18 +1455,22 @@ fn append_output( fn insert_output( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + ensure!(!args.is_empty(), "Shell command required"); shell(cx, &args.join(" "), &ShellBehavior::Insert); Ok(()) } -fn pipe( - cx: &mut compositor::Context, - args: &[Cow], - _event: PromptEvent, -) -> anyhow::Result<()> { +fn pipe(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + ensure!(!args.is_empty(), "Shell command required"); shell(cx, &args.join(" "), &ShellBehavior::Replace); Ok(()) @@ -1209,8 +1479,12 @@ fn pipe( fn run_shell_command( cx: &mut compositor::Context, args: &[Cow], - _event: PromptEvent, + event: PromptEvent, ) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + let shell = &cx.editor.config().shell; let (output, success) = shell_impl(shell, &args.join(" "), None)?; if success { @@ -1270,14 +1544,14 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ aliases: &["bc", "bclose"], doc: "Close the current buffer.", fun: buffer_close, - completer: Some(completers::buffer), + 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: Some(completers::buffer), + completer: Some(completers::buffer), }, TypableCommand { name: "buffer-close-others", @@ -1561,7 +1835,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ doc: "Display tree sitter scopes, primarily for theming and development.", fun: tree_sitter_scopes, completer: None, - }, + }, TypableCommand { name: "debug-start", aliases: &["dbg"], @@ -1787,10 +2061,6 @@ pub fn command_mode(cx: &mut Context) { } }, // completion move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { - if event != PromptEvent::Validate { - return; - } - let parts = input.split_whitespace().collect::>(); if parts.is_empty() { return; @@ -1811,10 +2081,10 @@ pub fn command_mode(cx: &mut Context) { if let Err(e) = (cmd.fun)(cx, &args[1..], event) { cx.editor.set_error(format!("{}", e)); } - } else { + } else if event == PromptEvent::Validate { cx.editor .set_error(format!("no such command: '{}'", parts[0])); - }; + } }, ); prompt.doc_fn = Box::new(|input: &str| { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 8d2bd3251..ca4cedb55 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -237,6 +237,7 @@ pub mod completers { )); names.push("default".into()); names.push("base16_default".into()); + names.sort(); let mut names: Vec<_> = names .into_iter() diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index f4dd234a4..375723e56 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -490,7 +490,7 @@ impl Component for Picker { _ => return EventResult::Ignored(None), }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _cx| { // remove the layer compositor.last_picker = compositor.pop(); }))); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 1ed27e996..a2943af98 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -464,7 +464,6 @@ pub struct Editor { pub registers: Registers, pub macro_recording: Option<(char, Vec)>, pub macro_replaying: Vec, - pub theme: Theme, pub language_servers: helix_lsp::Registry, pub diagnostics: BTreeMap>, @@ -476,6 +475,12 @@ pub struct Editor { pub syn_loader: Arc, pub theme_loader: Arc, + /// last_theme is used for theme previews. We store the current theme here, + /// and if previewing is cancelled, we can return to it. + pub last_theme: Option, + /// The currently applied editor theme. While previewing a theme, the previewed theme + /// is set here. + pub theme: Theme, pub status_msg: Option<(Cow<'static, str>, Severity)>, pub autoinfo: Option, @@ -500,6 +505,11 @@ pub enum ConfigEvent { Update(Box), } +enum ThemeAction { + Set, + Preview, +} + #[derive(Debug, Clone)] pub struct CompleteAction { pub trigger_offset: usize, @@ -544,6 +554,7 @@ impl Editor { breakpoints: HashMap::new(), syn_loader, theme_loader, + last_theme: None, registers: Registers::default(), clipboard_provider: get_clipboard_provider(), status_msg: None, @@ -613,7 +624,22 @@ impl Editor { .unwrap_or(false) } + pub fn unset_theme_preview(&mut self) { + if let Some(last_theme) = self.last_theme.take() { + self.set_theme(last_theme); + } + // None likely occurs when the user types ":theme" and then exits before previewing + } + + pub fn set_theme_preview(&mut self, theme: Theme) { + self.set_theme_impl(theme, ThemeAction::Preview); + } + pub fn set_theme(&mut self, theme: Theme) { + self.set_theme_impl(theme, ThemeAction::Set); + } + + fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) { // `ui.selection` is the only scope required to be able to render a theme. if theme.find_scope_index("ui.selection").is_none() { self.set_error("Invalid theme: `ui.selection` required"); @@ -623,7 +649,18 @@ impl Editor { let scopes = theme.scopes(); self.syn_loader.set_scopes(scopes.to_vec()); - self.theme = theme; + match preview { + ThemeAction::Preview => { + let last_theme = std::mem::replace(&mut self.theme, theme); + // only insert on first preview: this will be the last theme the user has saved + self.last_theme.get_or_insert(last_theme); + } + ThemeAction::Set => { + self.last_theme = None; + self.theme = theme; + } + } + self._refresh(); } diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 3f45aac6e..fa5fa7028 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -77,6 +77,14 @@ impl Loader { names } + pub fn default_theme(&self, true_color: bool) -> Theme { + if true_color { + self.default() + } else { + self.base16_default() + } + } + /// Returns the default theme pub fn default(&self) -> Theme { DEFAULT_THEME.clone()