aboutsummaryrefslogtreecommitdiff
path: root/helix-term
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term')
-rw-r--r--helix-term/src/application.rs44
-rw-r--r--helix-term/src/commands.rs294
-rw-r--r--helix-term/src/compositor.rs14
-rw-r--r--helix-term/src/config.rs84
-rw-r--r--helix-term/src/keymap.rs342
-rw-r--r--helix-term/src/main.rs14
-rw-r--r--helix-term/src/ui/completion.rs59
-rw-r--r--helix-term/src/ui/editor.rs12
-rw-r--r--helix-term/src/ui/markdown.rs32
-rw-r--r--helix-term/src/ui/mod.rs39
-rw-r--r--helix-term/src/ui/prompt.rs236
11 files changed, 927 insertions, 243 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index ce43808a..08853ed0 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -1,7 +1,8 @@
+use helix_core::syntax;
use helix_lsp::{lsp, LspProgressMap};
-use helix_view::{document::Mode, Document, Editor, Theme, View};
+use helix_view::{document::Mode, theme, Document, Editor, Theme, View};
-use crate::{args::Args, compositor::Compositor, config::Config, ui};
+use crate::{args::Args, compositor::Compositor, config::Config, keymap::Keymaps, ui};
use log::{error, info};
@@ -14,7 +15,7 @@ use std::{
time::Duration,
};
-use anyhow::Error;
+use anyhow::{Context, Error};
use crossterm::{
event::{Event, EventStream},
@@ -36,6 +37,8 @@ pub struct Application {
compositor: Compositor,
editor: Editor,
+ theme_loader: Arc<theme::Loader>,
+ syn_loader: Arc<syntax::Loader>,
callbacks: LspCallbacks,
lsp_progress: LspProgressMap,
@@ -47,9 +50,36 @@ impl Application {
use helix_view::editor::Action;
let mut compositor = Compositor::new()?;
let size = compositor.size();
- let mut editor = Editor::new(size);
- let mut editor_view = Box::new(ui::EditorView::new(config.keys));
+ let conf_dir = helix_core::config_dir();
+
+ let theme_loader =
+ std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
+
+ // load $HOME/.config/helix/languages.toml, fallback to default config
+ let lang_conf = std::fs::read(conf_dir.join("languages.toml"));
+ let lang_conf = lang_conf
+ .as_deref()
+ .unwrap_or(include_bytes!("../../languages.toml"));
+
+ let theme = if let Some(theme) = &config.global.theme {
+ match theme_loader.load(theme) {
+ Ok(theme) => theme,
+ Err(e) => {
+ log::warn!("failed to load theme `{}` - {}", theme, e);
+ theme_loader.default()
+ }
+ }
+ } else {
+ theme_loader.default()
+ };
+
+ let syn_loader_conf = toml::from_slice(lang_conf).expect("Could not parse languages.toml");
+ let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
+
+ let mut editor = Editor::new(size, theme_loader.clone(), syn_loader.clone());
+
+ let mut editor_view = Box::new(ui::EditorView::new(config.keymaps));
compositor.push(editor_view);
if !args.files.is_empty() {
@@ -72,10 +102,14 @@ impl Application {
editor.new_file(Action::VerticalSplit);
}
+ editor.set_theme(theme);
+
let mut app = Self {
compositor,
editor,
+ theme_loader,
+ syn_loader,
callbacks: FuturesUnordered::new(),
lsp_progress: LspProgressMap::new(),
lsp_progress_enabled: config.global.lsp_progress,
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index b006504b..28c4fe3a 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -11,7 +11,6 @@ use helix_core::{
use helix_view::{
document::{IndentStyle, Mode},
- input::{KeyCode, KeyEvent},
view::{View, PADDING},
Document, DocumentId, Editor, ViewId,
};
@@ -39,8 +38,8 @@ use std::{
path::{Path, PathBuf},
};
+use crossterm::event::{KeyCode, KeyEvent};
use once_cell::sync::Lazy;
-use serde::de::{self, Deserialize, Deserializer};
pub struct Context<'a> {
pub selected_register: helix_view::RegisterSelection,
@@ -186,7 +185,6 @@ impl Command {
search_next,
extend_search_next,
search_selection,
- select_line,
extend_line,
delete_selection,
change_selection,
@@ -223,9 +221,14 @@ impl Command {
undo,
redo,
yank,
+ yank_joined_to_clipboard,
+ yank_main_selection_to_clipboard,
replace_with_yanked,
+ replace_selections_with_clipboard,
paste_after,
paste_before,
+ paste_clipboard_after,
+ paste_clipboard_before,
indent,
unindent,
format_selections,
@@ -253,48 +256,6 @@ impl Command {
);
}
-impl fmt::Debug for Command {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let Command(name, _) = self;
- f.debug_tuple("Command").field(name).finish()
- }
-}
-
-impl fmt::Display for Command {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let Command(name, _) = self;
- f.write_str(name)
- }
-}
-
-impl std::str::FromStr for Command {
- type Err = anyhow::Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- Command::COMMAND_LIST
- .iter()
- .copied()
- .find(|cmd| cmd.0 == s)
- .ok_or_else(|| anyhow!("No command named '{}'", s))
- }
-}
-
-impl<'de> Deserialize<'de> for Command {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de>,
- {
- let s = String::deserialize(deserializer)?;
- s.parse().map_err(de::Error::custom)
- }
-}
-
-impl PartialEq for Command {
- fn eq(&self, other: &Self) -> bool {
- self.name() == other.name()
- }
-}
-
fn move_char_left(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
@@ -926,21 +887,6 @@ fn search_selection(cx: &mut Context) {
//
-fn select_line(cx: &mut Context) {
- let count = cx.count();
- let (view, doc) = current!(cx.editor);
-
- let pos = doc.selection(view.id).primary();
- let text = doc.text();
-
- let line = text.char_to_line(pos.head);
- let start = text.line_to_char(line);
- let end = text
- .line_to_char(std::cmp::min(doc.text().len_lines(), line + count))
- .saturating_sub(1);
-
- doc.set_selection(view.id, Selection::single(start, end));
-}
fn extend_line(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
@@ -1318,6 +1264,57 @@ mod cmd {
quit_all_impl(editor, args, event, true)
}
+ fn theme(editor: &mut Editor, args: &[&str], event: PromptEvent) {
+ let theme = if let Some(theme) = args.first() {
+ theme
+ } else {
+ editor.set_error("theme name not provided".into());
+ return;
+ };
+
+ editor.set_theme_from_name(theme);
+ }
+
+ fn yank_main_selection_to_clipboard(editor: &mut Editor, _: &[&str], _: PromptEvent) {
+ yank_main_selection_to_clipboard_impl(editor);
+ }
+
+ fn yank_joined_to_clipboard(editor: &mut Editor, args: &[&str], _: PromptEvent) {
+ let separator = args.first().copied().unwrap_or("\n");
+ yank_joined_to_clipboard_impl(editor, separator);
+ }
+
+ fn paste_clipboard_after(editor: &mut Editor, _: &[&str], _: PromptEvent) {
+ paste_clipboard_impl(editor, Paste::After);
+ }
+
+ fn paste_clipboard_before(editor: &mut Editor, _: &[&str], _: PromptEvent) {
+ paste_clipboard_impl(editor, Paste::After);
+ }
+
+ fn replace_selections_with_clipboard(editor: &mut Editor, _: &[&str], _: PromptEvent) {
+ let (view, doc) = current!(editor);
+
+ match editor.clipboard_provider.get_contents() {
+ Ok(contents) => {
+ let transaction =
+ Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
+ let max_to = doc.text().len_chars().saturating_sub(1);
+ let to = std::cmp::min(max_to, range.to() + 1);
+ (range.from(), to, Some(contents.as_str().into()))
+ });
+
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+ }
+ Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
+ }
+ }
+
+ fn show_clipboard_provider(editor: &mut Editor, _: &[&str], _: PromptEvent) {
+ editor.set_status(editor.clipboard_provider.name().into());
+ }
+
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
@@ -1431,7 +1428,55 @@ mod cmd {
fun: force_quit_all,
completer: None,
},
-
+ TypableCommand {
+ name: "theme",
+ alias: None,
+ doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)",
+ fun: theme,
+ completer: Some(completers::theme),
+ },
+ TypableCommand {
+ name: "clipboard-yank",
+ alias: None,
+ doc: "Yank main selection into system clipboard.",
+ fun: yank_main_selection_to_clipboard,
+ completer: None,
+ },
+ TypableCommand {
+ name: "clipboard-yank-join",
+ alias: None,
+ doc: "Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline.", // FIXME: current UI can't display long doc.
+ fun: yank_joined_to_clipboard,
+ completer: None,
+ },
+ TypableCommand {
+ name: "clipboard-paste-after",
+ alias: None,
+ doc: "Paste system clipboard after selections.",
+ fun: paste_clipboard_after,
+ completer: None,
+ },
+ TypableCommand {
+ name: "clipboard-paste-before",
+ alias: None,
+ doc: "Paste system clipboard before selections.",
+ fun: paste_clipboard_before,
+ completer: None,
+ },
+ TypableCommand {
+ name: "clipboard-paste-replace",
+ alias: None,
+ doc: "Replace selections with content of system clipboard.",
+ fun: replace_selections_with_clipboard,
+ completer: None,
+ },
+ TypableCommand {
+ name: "show-clipboard-provider",
+ alias: None,
+ doc: "Show clipboard provider name in status bar.",
+ fun: show_clipboard_provider,
+ completer: None,
+ },
];
pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -2424,6 +2469,52 @@ fn yank(cx: &mut Context) {
cx.editor.set_status(msg)
}
+fn yank_joined_to_clipboard_impl(editor: &mut Editor, separator: &str) {
+ let (view, doc) = current!(editor);
+
+ let values: Vec<String> = doc
+ .selection(view.id)
+ .fragments(doc.text().slice(..))
+ .map(Cow::into_owned)
+ .collect();
+
+ let msg = format!(
+ "joined and yanked {} selection(s) to system clipboard",
+ values.len(),
+ );
+
+ let joined = values.join(separator);
+
+ if let Err(e) = editor.clipboard_provider.set_contents(joined) {
+ log::error!("Couldn't set system clipboard content: {:?}", e);
+ }
+
+ editor.set_status(msg);
+}
+
+fn yank_joined_to_clipboard(cx: &mut Context) {
+ yank_joined_to_clipboard_impl(&mut cx.editor, "\n");
+}
+
+fn yank_main_selection_to_clipboard_impl(editor: &mut Editor) {
+ let (view, doc) = current!(editor);
+
+ let value = doc
+ .selection(view.id)
+ .primary()
+ .fragment(doc.text().slice(..));
+
+ if let Err(e) = editor.clipboard_provider.set_contents(value.into_owned()) {
+ log::error!("Couldn't set system clipboard content: {:?}", e);
+ }
+
+ editor.set_status("yanked main selection to system clipboard".to_owned());
+}
+
+fn yank_main_selection_to_clipboard(cx: &mut Context) {
+ yank_main_selection_to_clipboard_impl(&mut cx.editor);
+}
+
#[derive(Copy, Clone)]
enum Paste {
Before,
@@ -2469,6 +2560,31 @@ fn paste_impl(
Some(transaction)
}
+fn paste_clipboard_impl(editor: &mut Editor, action: Paste) {
+ let (view, doc) = current!(editor);
+
+ match editor
+ .clipboard_provider
+ .get_contents()
+ .map(|contents| paste_impl(&[contents], doc, view, action))
+ {
+ Ok(Some(transaction)) => {
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+ }
+ Ok(None) => {}
+ Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
+ }
+}
+
+fn paste_clipboard_after(cx: &mut Context) {
+ paste_clipboard_impl(&mut cx.editor, Paste::After);
+}
+
+fn paste_clipboard_before(cx: &mut Context) {
+ paste_clipboard_impl(&mut cx.editor, Paste::Before);
+}
+
fn replace_with_yanked(cx: &mut Context) {
let reg_name = cx.selected_register.name();
let (view, doc) = current!(cx.editor);
@@ -2489,6 +2605,29 @@ fn replace_with_yanked(cx: &mut Context) {
}
}
+fn replace_selections_with_clipboard_impl(editor: &mut Editor) {
+ let (view, doc) = current!(editor);
+
+ match editor.clipboard_provider.get_contents() {
+ Ok(contents) => {
+ let transaction =
+ Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
+ let max_to = doc.text().len_chars().saturating_sub(1);
+ let to = std::cmp::min(max_to, range.to() + 1);
+ (range.from(), to, Some(contents.as_str().into()))
+ });
+
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+ }
+ Err(e) => log::error!("Couldn't get system clipboard contents: {:?}", e),
+ }
+}
+
+fn replace_selections_with_clipboard(cx: &mut Context) {
+ replace_selections_with_clipboard_impl(&mut cx.editor);
+}
+
// alt-p => paste every yanked selection after selected text
// alt-P => paste every yanked selection before selected text
// R => replace selected text with yanked text
@@ -2854,7 +2993,7 @@ fn hover(cx: &mut Context) {
// skip if contents empty
- let contents = ui::Markdown::new(contents);
+ let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
let mut popup = Popup::new(contents);
compositor.push(Box::new(popup));
}
@@ -3009,6 +3148,11 @@ fn space_mode(cx: &mut Context) {
'b' => buffer_picker(cx),
's' => symbol_picker(cx),
'w' => window_mode(cx),
+ 'y' => yank_joined_to_clipboard(cx),
+ 'Y' => yank_main_selection_to_clipboard(cx),
+ 'p' => paste_clipboard_after(cx),
+ 'P' => paste_clipboard_before(cx),
+ 'R' => replace_selections_with_clipboard(cx),
// ' ' => toggle_alternate_buffer(cx),
// TODO: temporary since space mode took its old key
' ' => keep_primary_selection(cx),
@@ -3092,3 +3236,29 @@ fn right_bracket_mode(cx: &mut Context) {
}
})
}
+
+impl fmt::Display for Command {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Command(name, _) = self;
+ f.write_str(name)
+ }
+}
+
+impl std::str::FromStr for Command {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ Command::COMMAND_LIST
+ .iter()
+ .copied()
+ .find(|cmd| cmd.0 == s)
+ .ok_or_else(|| anyhow!("No command named '{}'", s))
+ }
+}
+
+impl fmt::Debug for Command {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Command(name, _) = self;
+ f.debug_tuple("Command").field(name).finish()
+ }
+}
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index 6b39bb62..0e6a313d 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -178,13 +178,13 @@ pub trait AnyComponent {
/// Returns a boxed any from a boxed self.
///
/// Can be used before `Box::downcast()`.
- ///
- /// # Examples
- ///
- /// ```rust
- /// // let boxed: Box<Component> = Box::new(TextComponent::new("text"));
- /// // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
- /// ```
+ //
+ // # Examples
+ //
+ // ```rust
+ // let boxed: Box<Component> = Box::new(TextComponent::new("text"));
+ // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
+ // ```
fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
}
diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs
index 9c962299..2c95fae3 100644
--- a/helix-term/src/config.rs
+++ b/helix-term/src/config.rs
@@ -1,63 +1,55 @@
-use serde::Deserialize;
+use anyhow::{Error, Result};
+use std::{collections::HashMap, str::FromStr};
-use crate::commands::Command;
-use crate::keymap::Keymaps;
+use serde::{de::Error as SerdeError, Deserialize, Serialize};
+
+use crate::keymap::{parse_keymaps, Keymaps};
-#[derive(Debug, PartialEq, Deserialize)]
pub struct GlobalConfig {
+ pub theme: Option<String>,
pub lsp_progress: bool,
}
impl Default for GlobalConfig {
fn default() -> Self {
- Self { lsp_progress: true }
+ Self {
+ lsp_progress: true,
+ theme: None,
+ }
}
}
-#[derive(Debug, Default, PartialEq, Deserialize)]
-#[serde(default)]
+#[derive(Default)]
pub struct Config {
pub global: GlobalConfig,
- pub keys: Keymaps,
+ pub keymaps: Keymaps,
}
-#[test]
-fn parsing_keymaps_config_file() {
- use helix_core::hashmap;
- use helix_view::document::Mode;
- use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
-
- let sample_keymaps = r#"
- [keys.insert]
- y = "move_line_down"
- S-C-a = "delete_selection"
-
- [keys.normal]
- A-F12 = "move_next_word_end"
- "#;
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+struct TomlConfig {
+ theme: Option<String>,
+ lsp_progress: Option<bool>,
+ keys: Option<HashMap<String, HashMap<String, String>>>,
+}
- assert_eq!(
- toml::from_str::<Config>(sample_keymaps).unwrap(),
- Config {
- global: Default::default(),
- keys: Keymaps(hashmap! {
- Mode::Insert => hashmap! {
- KeyEvent {
- code: KeyCode::Char('y'),
- modifiers: KeyModifiers::NONE,
- } => Command::move_line_down,
- KeyEvent {
- code: KeyCode::Char('a'),
- modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL,
- } => Command::delete_selection,
- },
- Mode::Normal => hashmap! {
- KeyEvent {
- code: KeyCode::F(12),
- modifiers: KeyModifiers::ALT,
- } => Command::move_next_word_end,
- },
- })
- }
- );
+impl<'de> Deserialize<'de> for Config {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let config = TomlConfig::deserialize(deserializer)?;
+ Ok(Self {
+ global: GlobalConfig {
+ lsp_progress: config.lsp_progress.unwrap_or(true),
+ theme: config.theme,
+ },
+ keymaps: config
+ .keys
+ .map(|r| parse_keymaps(&r))
+ .transpose()
+ .map_err(|e| D::Error::custom(format!("Error deserializing keymap: {}", e)))?
+ .unwrap_or_else(Keymaps::default),
+ })
+ }
}
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 24924832..46d495c3 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -3,8 +3,6 @@ pub use crate::commands::Command;
use anyhow::{anyhow, Error, Result};
use helix_core::hashmap;
use helix_view::document::Mode;
-use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
-use serde::Deserialize;
use std::{
collections::HashMap,
fmt::Display,
@@ -101,6 +99,14 @@ use std::{
// D] = last diagnostic
// }
+// #[cfg(feature = "term")]
+pub use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
+
+#[derive(Clone, Debug)]
+pub struct Keymap(pub HashMap<KeyEvent, Command>);
+#[derive(Clone, Debug)]
+pub struct Keymaps(pub HashMap<Mode, Keymap>);
+
#[macro_export]
macro_rules! key {
($key:ident) => {
@@ -135,21 +141,9 @@ macro_rules! alt {
};
}
-#[derive(Debug, PartialEq, Deserialize)]
-#[serde(transparent)]
-pub struct Keymaps(pub HashMap<Mode, HashMap<KeyEvent, Command>>);
-
-impl Deref for Keymaps {
- type Target = HashMap<Mode, HashMap<KeyEvent, Command>>;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
impl Default for Keymaps {
- fn default() -> Keymaps {
- let normal = hashmap!(
+ fn default() -> Self {
+ let normal = Keymap(hashmap!(
key!('h') => Command::move_char_left,
key!('j') => Command::move_line_down,
key!('k') => Command::move_line_up,
@@ -202,9 +196,7 @@ impl Default for Keymaps {
key!(';') => Command::collapse_selection,
alt!(';') => Command::flip_selections,
key!('%') => Command::select_all,
- key!('x') => Command::select_line,
- key!('X') => Command::extend_line,
- // or select mode X?
+ key!('x') => Command::extend_line,
// extend_to_whole_line, crop_to_whole_line
@@ -283,12 +275,12 @@ impl Default for Keymaps {
key!('z') => Command::view_mode,
key!('"') => Command::select_register,
- );
+ ));
// TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
// we keep this separate select mode. More keys can fit into normal mode then, but it's weird
// because some selection operations can now be done from normal mode, some from select mode.
let mut select = normal.clone();
- select.extend(
+ select.0.extend(
hashmap!(
key!('h') => Command::extend_char_left,
key!('j') => Command::extend_line_down,
@@ -321,7 +313,7 @@ impl Default for Keymaps {
// TODO: select could be normal mode with some bindings merged over
Mode::Normal => normal,
Mode::Select => select,
- Mode::Insert => hashmap!(
+ Mode::Insert => Keymap(hashmap!(
key!(Esc) => Command::normal_mode as Command,
key!(Backspace) => Command::delete_char_backward,
key!(Delete) => Command::delete_char_forward,
@@ -333,9 +325,313 @@ impl Default for Keymaps {
key!(Right) => Command::move_char_right,
key!(PageUp) => Command::page_up,
key!(PageDown) => Command::page_down,
+ key!(Home) => Command::move_line_start,
+ key!(End) => Command::move_line_end,
ctrl!('x') => Command::completion,
ctrl!('w') => Command::delete_word_backward,
- ),
+ )),
))
}
}
+
+// Newtype wrapper over keys to allow toml serialization/parsing
+#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Hash)]
+pub struct RepresentableKeyEvent(pub KeyEvent);
+impl Display for RepresentableKeyEvent {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self(key) = self;
+ f.write_fmt(format_args!(
+ "{}{}{}",
+ if key.modifiers.contains(KeyModifiers::SHIFT) {
+ "S-"
+ } else {
+ ""
+ },
+ if key.modifiers.contains(KeyModifiers::ALT) {
+ "A-"
+ } else {
+ ""
+ },
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
+ "C-"
+ } else {
+ ""
+ },
+ ))?;
+ match key.code {
+ KeyCode::Backspace => f.write_str("backspace")?,
+ KeyCode::Enter => f.write_str("ret")?,
+ KeyCode::Left => f.write_str("left")?,
+ KeyCode::Right => f.write_str("right")?,
+ KeyCode::Up => f.write_str("up")?,
+ KeyCode::Down => f.write_str("down")?,
+ KeyCode::Home => f.write_str("home")?,
+ KeyCode::End => f.write_str("end")?,
+ KeyCode::PageUp => f.write_str("pageup")?,
+ KeyCode::PageDown => f.write_str("pagedown")?,
+ KeyCode::Tab => f.write_str("tab")?,
+ KeyCode::BackTab => f.write_str("backtab")?,
+ KeyCode::Delete => f.write_str("del")?,
+ KeyCode::Insert => f.write_str("ins")?,
+ KeyCode::Null => f.write_str("null")?,
+ KeyCode::Esc => f.write_str("esc")?,
+ KeyCode::Char('<') => f.write_str("lt")?,
+ KeyCode::Char('>') => f.write_str("gt")?,
+ KeyCode::Char('+') => f.write_str("plus")?,
+ KeyCode::Char('-') => f.write_str("minus")?,
+ KeyCode::Char(';') => f.write_str("semicolon")?,
+ KeyCode::Char('%') => f.write_str("percent")?,
+ KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
+ KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
+ };
+ Ok(())
+ }
+}
+
+impl FromStr for RepresentableKeyEvent {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut tokens: Vec<_> = s.split('-').collect();
+ let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
+ "backspace" => KeyCode::Backspace,
+ "space" => KeyCode::Char(' '),
+ "ret" => KeyCode::Enter,
+ "lt" => KeyCode::Char('<'),
+ "gt" => KeyCode::Char('>'),
+ "plus" => KeyCode::Char('+'),
+ "minus" => KeyCode::Char('-'),
+ "semicolon" => KeyCode::Char(';'),
+ "percent" => KeyCode::Char('%'),
+ "left" => KeyCode::Left,
+ "right" => KeyCode::Right,
+ "up" => KeyCode::Down,
+ "home" => KeyCode::Home,
+ "end" => KeyCode::End,
+ "pageup" => KeyCode::PageUp,
+ "pagedown" => KeyCode::PageDown,
+ "tab" => KeyCode::Tab,
+ "backtab" => KeyCode::BackTab,
+ "del" => KeyCode::Delete,
+ "ins" => KeyCode::Insert,
+ "null" => KeyCode::Null,
+ "esc" => KeyCode::Esc,
+ single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
+ function if function.len() > 1 && function.starts_with('F') => {
+ let function: String = function.chars().skip(1).collect();
+ let function = str::parse::<u8>(&function)?;
+ (function > 0 && function < 13)
+ .then(|| KeyCode::F(function))
+ .ok_or_else(|| anyhow!("Invalid function key '{}'", function))?
+ }
+ invalid => return Err(anyhow!("Invalid key code '{}'", invalid)),
+ };
+
+ let mut modifiers = KeyModifiers::empty();
+ for token in tokens {
+ let flag = match token {
+ "S" => KeyModifiers::SHIFT,
+ "A" => KeyModifiers::ALT,
+ "C" => KeyModifiers::CONTROL,
+ _ => return Err(anyhow!("Invalid key modifier '{}-'", token)),
+ };
+
+ if modifiers.contains(flag) {
+ return Err(anyhow!("Repeated key modifier '{}-'", token));
+ }
+ modifiers.insert(flag);
+ }
+
+ Ok(RepresentableKeyEvent(KeyEvent { code, modifiers }))
+ }
+}
+
+pub fn parse_keymaps(toml_keymaps: &HashMap<String, HashMap<String, String>>) -> Result<Keymaps> {
+ let mut keymaps = Keymaps::default();
+
+ for (mode, map) in toml_keymaps {
+ let mode = Mode::from_str(&mode)?;
+ for (key, command) in map {
+ let key = str::parse::<RepresentableKeyEvent>(&key)?;
+ let command = str::parse::<Command>(&command)?;
+ keymaps.0.get_mut(&mode).unwrap().0.insert(key.0, command);
+ }
+ }
+ Ok(keymaps)
+}
+
+impl Deref for Keymap {
+ type Target = HashMap<KeyEvent, Command>;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl Deref for Keymaps {
+ type Target = HashMap<Mode, Keymap>;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl DerefMut for Keymap {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl DerefMut for Keymaps {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::config::Config;
+
+ use super::*;
+
+ impl PartialEq for Command {
+ fn eq(&self, other: &Self) -> bool {
+ self.name() == other.name()
+ }
+ }
+
+ #[test]
+ fn parsing_keymaps_config_file() {
+ let sample_keymaps = r#"
+ [keys.insert]
+ y = "move_line_down"
+ S-C-a = "delete_selection"
+
+ [keys.normal]
+ A-F12 = "move_next_word_end"
+ "#;
+
+ let config: Config = toml::from_str(sample_keymaps).unwrap();
+ assert_eq!(
+ *config
+ .keymaps
+ .0
+ .get(&Mode::Insert)
+ .unwrap()
+ .0
+ .get(&KeyEvent {
+ code: KeyCode::Char('y'),
+ modifiers: KeyModifiers::NONE
+ })
+ .unwrap(),
+ Command::move_line_down
+ );
+ assert_eq!(
+ *config
+ .keymaps
+ .0
+ .get(&Mode::Insert)
+ .unwrap()
+ .0
+ .get(&KeyEvent {
+ code: KeyCode::Char('a'),
+ modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
+ })
+ .unwrap(),
+ Command::delete_selection
+ );
+ assert_eq!(
+ *config
+ .keymaps
+ .0
+ .get(&Mode::Normal)
+ .unwrap()
+ .0
+ .get(&KeyEvent {
+ code: KeyCode::F(12),
+ modifiers: KeyModifiers::ALT
+ })
+ .unwrap(),
+ Command::move_next_word_end
+ );
+ }
+
+ #[test]
+ fn parsing_unmodified_keys() {
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("backspace").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::Backspace,
+ modifiers: KeyModifiers::NONE
+ })
+ );
+
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("left").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::Left,
+ modifiers: KeyModifiers::NONE
+ })
+ );
+
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>(",").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::Char(','),
+ modifiers: KeyModifiers::NONE
+ })
+ );
+
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("w").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::Char('w'),
+ modifiers: KeyModifiers::NONE
+ })
+ );
+
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("F12").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::F(12),
+ modifiers: KeyModifiers::NONE
+ })
+ );
+ }
+
+ fn parsing_modified_keys() {
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("S-minus").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::Char('-'),
+ modifiers: KeyModifiers::SHIFT
+ })
+ );
+
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("C-A-S-F12").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::F(12),
+ modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT
+ })
+ );
+
+ assert_eq!(
+ str::parse::<RepresentableKeyEvent>("S-C-2").unwrap(),
+ RepresentableKeyEvent(KeyEvent {
+ code: KeyCode::F(2),
+ modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
+ })
+ );
+ }
+
+ #[test]
+ fn parsing_nonsensical_keys_fails() {
+ assert!(str::parse::<RepresentableKeyEvent>("F13").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("F0").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("aaa").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("S-S-a").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("C-A-S-C-1").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("FU").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("123").is_err());
+ assert!(str::parse::<RepresentableKeyEvent>("S--").is_err());
+ }
+}
diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs
index 12176910..ef912480 100644
--- a/helix-term/src/main.rs
+++ b/helix-term/src/main.rs
@@ -1,9 +1,10 @@
-use anyhow::{Context, Error, Result};
use helix_term::application::Application;
use helix_term::args::Args;
use helix_term::config::Config;
use std::path::PathBuf;
+use anyhow::{Context, Result};
+
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
let mut base_config = fern::Dispatch::new();
@@ -88,11 +89,12 @@ FLAGS:
std::fs::create_dir_all(&conf_dir).ok();
}
- let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
- Ok(config) => toml::from_str(&config)?,
- Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
- Err(err) => return Err(Error::new(err)),
- };
+ let config = std::fs::read_to_string(conf_dir.join("config.toml"))
+ .ok()
+ .map(|s| toml::from_str(&s))
+ .transpose()?
+ .or_else(|| Some(Config::default()))
+ .unwrap();
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 06ed966d..80f7d590 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -238,6 +238,9 @@ impl Component for Completion {
.language()
.and_then(|scope| scope.strip_prefix("source."))
.unwrap_or("");
+ let cursor_pos = doc.selection(view.id).cursor();
+ let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
+ - view.first_line) as u16;
let doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
@@ -246,42 +249,60 @@ impl Component for Completion {
value: contents,
})) => {
// TODO: convert to wrapped text
- Markdown::new(format!(
- "```{}\n{}\n```\n{}",
- language,
- option.detail.as_deref().unwrap_or_default(),
- contents.clone()
- ))
+ Markdown::new(
+ format!(
+ "```{}\n{}\n```\n{}",
+ language,
+ option.detail.as_deref().unwrap_or_default(),
+ contents.clone()
+ ),
+ cx.editor.syn_loader.clone(),
+ )
}
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: contents,
})) => {
// TODO: set language based on doc scope
- Markdown::new(format!(
- "```{}\n{}\n```\n{}",
- language,
- option.detail.as_deref().unwrap_or_default(),
- contents.clone()
- ))
+ Markdown::new(
+ format!(
+ "```{}\n{}\n```\n{}",
+ language,
+ option.detail.as_deref().unwrap_or_default(),
+ contents.clone()
+ ),
+ cx.editor.syn_loader.clone(),
+ )
}
None if option.detail.is_some() => {
// TODO: copied from above
// TODO: set language based on doc scope
- Markdown::new(format!(
- "```{}\n{}\n```",
- language,
- option.detail.as_deref().unwrap_or_default(),
- ))
+ Markdown::new(
+ format!(
+ "```{}\n{}\n```",
+ language,
+ option.detail.as_deref().unwrap_or_default(),
+ ),
+ cx.editor.syn_loader.clone(),
+ )
}
None => return,
};
let half = area.height / 2;
let height = 15.min(half);
- // -2 to subtract command line + statusline. a bit of a hack, because of splits.
- let area = Rect::new(0, area.height - height - 2, area.width, height);
+ // we want to make sure the cursor is visible (not hidden behind the documentation)
+ let y = if cursor_pos + view.area.y
+ >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
+ {
+ 0
+ } else {
+ // -2 to subtract command line + statusline. a bit of a hack, because of splits.
+ area.height.saturating_sub(height).saturating_sub(2)
+ };
+
+ let area = Rect::new(0, y, area.width, height);
// clear area
let background = cx.editor.theme.get("ui.popup");
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index da8f0f53..faede58c 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -11,13 +11,12 @@ use helix_core::{
syntax::{self, HighlightEvent},
LineEnding, Position, Range,
};
-use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
use helix_view::{document::Mode, Document, Editor, Theme, View};
use std::borrow::Cow;
use crossterm::{
cursor,
- event::{read, Event, EventStream},
+ event::{read, Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
};
use tui::{
backend::CrosstermBackend,
@@ -130,7 +129,7 @@ impl EditorView {
})],
};
let mut spans = Vec::new();
- let mut visual_x = 0;
+ let mut visual_x = 0u16;
let mut line = 0u16;
let tab_width = doc.tab_width();
@@ -186,7 +185,7 @@ impl EditorView {
break 'outer;
}
} else if grapheme == "\t" {
- visual_x += (tab_width as u16);
+ visual_x = visual_x.saturating_add(tab_width as u16);
} else {
let out_of_bounds = visual_x < view.first_col as u16
|| visual_x >= viewport.width + view.first_col as u16;
@@ -198,7 +197,7 @@ impl EditorView {
if out_of_bounds {
// if we're offscreen just keep going until we hit a new line
- visual_x += width;
+ visual_x = visual_x.saturating_add(width);
continue;
}
@@ -608,8 +607,7 @@ impl Component for EditorView {
cx.editor.resize(Rect::new(0, 0, width, height - 1));
EventResult::Consumed(None)
}
- Event::Key(key) => {
- let mut key = KeyEvent::from(key);
+ Event::Key(mut key) => {
canonicalize_key(&mut key);
// clear status
cx.editor.status_msg = None;
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index 3ce3a5b8..72a3e4ff 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -7,25 +7,34 @@ use tui::{
text::Text,
};
-use std::borrow::Cow;
+use std::{borrow::Cow, sync::Arc};
-use helix_core::Position;
+use helix_core::{syntax, Position};
use helix_view::{Editor, Theme};
pub struct Markdown {
contents: String,
+
+ config_loader: Arc<syntax::Loader>,
}
// TODO: pre-render and self reference via Pin
// better yet, just use Tendril + subtendril for references
impl Markdown {
- pub fn new(contents: String) -> Self {
- Self { contents }
+ pub fn new(contents: String, config_loader: Arc<syntax::Loader>) -> Self {
+ Self {
+ contents,
+ config_loader,
+ }
}
}
-fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
+fn parse<'a>(
+ contents: &'a str,
+ theme: Option<&Theme>,
+ loader: &syntax::Loader,
+) -> tui::text::Text<'a> {
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
use tui::text::{Span, Spans, Text};
@@ -79,9 +88,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
use helix_core::Rope;
let rope = Rope::from(text.as_ref());
- let syntax = syntax::LOADER
- .get()
- .unwrap()
+ let syntax = loader
.language_config_for_scope(&format!("source.{}", language))
.and_then(|config| config.highlight_config(theme.scopes()))
.map(|config| Syntax::new(&rope, config));
@@ -101,9 +108,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
}
HighlightEvent::Source { start, end } => {
let style = match highlights.first() {
- Some(span) => {
- theme.get(theme.scopes()[span.0].as_str())
- }
+ Some(span) => theme.get(&theme.scopes()[span.0]),
None => text_style,
};
@@ -159,7 +164,6 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
}
}
Event::Code(text) | Event::Html(text) => {
- log::warn!("code {:?}", text);
let mut span = to_span(text);
span.style = code_style;
spans.push(span);
@@ -198,7 +202,7 @@ impl Component for Markdown {
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
use tui::widgets::{Paragraph, Widget, Wrap};
- let text = parse(&self.contents, Some(&cx.editor.theme));
+ let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
let par = Paragraph::new(text)
.wrap(Wrap { trim: false })
@@ -209,7 +213,7 @@ impl Component for Markdown {
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
- let contents = parse(&self.contents, None);
+ let contents = parse(&self.contents, None, &self.config_loader);
let padding = 2;
let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 39e11cd6..e0177b7c 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -115,10 +115,43 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
pub mod completers {
use crate::ui::prompt::Completion;
- use std::borrow::Cow;
+ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
+ use fuzzy_matcher::FuzzyMatcher;
+ use helix_view::theme;
+ use std::cmp::Reverse;
+ use std::{borrow::Cow, sync::Arc};
pub type Completer = fn(&str) -> Vec<Completion>;
+ pub fn theme(input: &str) -> Vec<Completion> {
+ 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"),
+ ));
+ names.push("default".into());
+
+ let mut names: Vec<_> = names
+ .into_iter()
+ .map(|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
+ }
+
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
pub fn filename(input: &str) -> Vec<Completion> {
// Rust's filename handling is really annoying.
@@ -178,10 +211,6 @@ pub mod completers {
// if empty, return a list of dirs and files in current dir
if let Some(file_name) = file_name {
- use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
- use fuzzy_matcher::FuzzyMatcher;
- use std::cmp::Reverse;
-
let matcher = Matcher::default();
// inefficient, but we need to calculate the scores, filter out None, then sort.
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index 991b328d..7ca4308c 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -6,6 +6,11 @@ use helix_view::{Editor, Theme};
use std::{borrow::Cow, ops::RangeFrom};
use tui::terminal::CursorKind;
+use helix_core::{
+ unicode::segmentation::{GraphemeCursor, GraphemeIncomplete},
+ unicode::width::UnicodeWidthStr,
+};
+
pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
pub struct Prompt {
@@ -34,6 +39,17 @@ pub enum CompletionDirection {
Backward,
}
+#[derive(Debug, Clone, Copy)]
+pub enum Movement {
+ BackwardChar(usize),
+ BackwardWord(usize),
+ ForwardChar(usize),
+ ForwardWord(usize),
+ StartOfLine,
+ EndOfLine,
+ None,
+}
+
impl Prompt {
pub fn new(
prompt: String,
@@ -52,30 +68,120 @@ impl Prompt {
}
}
+ /// 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 {
+ match movement {
+ Movement::BackwardChar(rep) => {
+ let mut position = self.cursor;
+ for _ in 0..rep {
+ let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
+ if let Ok(Some(pos)) = cursor.prev_boundary(&self.line, 0) {
+ position = pos;
+ } else {
+ break;
+ }
+ }
+ position
+ }
+ Movement::BackwardWord(rep) => {
+ let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
+ if char_indices.is_empty() {
+ return self.cursor;
+ }
+ let mut char_position = char_indices
+ .iter()
+ .position(|(idx, _)| *idx == self.cursor)
+ .unwrap_or(char_indices.len() - 1);
+
+ for _ in 0..rep {
+ if char_position == 0 {
+ break;
+ }
+
+ let mut found = None;
+ for prev in (0..char_position - 1).rev() {
+ if char_indices[prev].1.is_whitespace() {
+ found = Some(prev + 1);
+ break;
+ }
+ }
+
+ char_position = found.unwrap_or(0);
+ }
+ char_indices[char_position].0
+ }
+ Movement::ForwardWord(rep) => {
+ let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
+ if char_indices.is_empty() {
+ return self.cursor;
+ }
+ let mut char_position = char_indices
+ .iter()
+ .position(|(idx, _)| *idx == self.cursor)
+ .unwrap_or_else(|| char_indices.len());
+
+ for _ in 0..rep {
+ // Skip any non-whitespace characters
+ while char_position < char_indices.len()
+ && !char_indices[char_position].1.is_whitespace()
+ {
+ char_position += 1;
+ }
+
+ // Skip any whitespace characters
+ while char_position < char_indices.len()
+ && char_indices[char_position].1.is_whitespace()
+ {
+ char_position += 1;
+ }
+
+ // We are now on the start of the next word
+ }
+ char_indices
+ .get(char_position)
+ .map(|(i, _)| *i)
+ .unwrap_or_else(|| self.line.len())
+ }
+ Movement::ForwardChar(rep) => {
+ let mut position = self.cursor;
+ for _ in 0..rep {
+ let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
+ if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
+ position = pos;
+ } else {
+ break;
+ }
+ }
+ position
+ }
+ Movement::StartOfLine => 0,
+ Movement::EndOfLine => {
+ let mut cursor =
+ GraphemeCursor::new(self.line.len().saturating_sub(1), self.line.len(), false);
+ if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
+ pos
+ } else {
+ self.cursor
+ }
+ }
+ Movement::None => self.cursor,
+ }
+ }
+
pub fn insert_char(&mut self, c: char) {
- let pos = if self.line.is_empty() {
- 0
- } else {
- self.line
- .char_indices()
- .nth(self.cursor)
- .map(|(pos, _)| pos)
- .unwrap_or_else(|| self.line.len())
- };
- self.line.insert(pos, c);
- self.cursor += 1;
+ 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.exit_selection();
}
- pub fn move_char_left(&mut self) {
- self.cursor = self.cursor.saturating_sub(1)
- }
-
- pub fn move_char_right(&mut self) {
- if self.cursor < self.line.len() {
- self.cursor += 1;
- }
+ pub fn move_cursor(&mut self, movement: Movement) {
+ let pos = self.eval_movement(movement);
+ self.cursor = pos
}
pub fn move_start(&mut self) {
@@ -87,39 +193,29 @@ impl Prompt {
}
pub fn delete_char_backwards(&mut self) {
- if self.cursor > 0 {
- let pos = self
- .line
- .char_indices()
- .nth(self.cursor - 1)
- .map(|(pos, _)| pos)
- .expect("line is not empty");
- self.line.remove(pos);
- self.cursor -= 1;
- self.completion = (self.completion_fn)(&self.line);
- }
+ 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);
}
pub fn delete_word_backwards(&mut self) {
- use helix_core::get_general_category;
- let mut chars = self.line.char_indices().rev();
- // TODO add skipping whitespace logic here
- let (mut i, cat) = match chars.next() {
- Some((i, c)) => (i, get_general_category(c)),
- None => return,
- };
- self.cursor -= 1;
- for (nn, nc) in chars {
- if get_general_category(nc) != cat {
- break;
- }
- i = nn;
- self.cursor -= 1;
- }
- self.line.drain(i..);
+ 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);
+ }
+
+ pub fn kill_to_end_of_line(&mut self) {
+ let pos = self.eval_movement(Movement::EndOfLine);
+ self.line.replace_range(self.cursor..pos, "");
+
self.exit_selection();
+ self.completion = (self.completion_fn)(&self.line);
}
pub fn clear(&mut self) {
@@ -293,32 +389,72 @@ impl Component for Prompt {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
}
KeyEvent {
+ code: KeyCode::Char('c'),
+ modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
code: KeyCode::Esc, ..
} => {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort);
return close_fn;
}
KeyEvent {
+ code: KeyCode::Char('f'),
+ modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
code: KeyCode::Right,
..
- } => self.move_char_right(),
+ } => self.move_cursor(Movement::ForwardChar(1)),
KeyEvent {
+ code: KeyCode::Char('b'),
+ modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
code: KeyCode::Left,
..
- } => self.move_char_left(),
+ } => self.move_cursor(Movement::BackwardChar(1)),
KeyEvent {
+ code: KeyCode::End,
+ modifiers: KeyModifiers::NONE,
+ }
+ | KeyEvent {
code: KeyCode::Char('e'),
modifiers: KeyModifiers::CONTROL,
} => self.move_end(),
KeyEvent {
+ code: KeyCode::Home,
+ modifiers: KeyModifiers::NONE,
+ }
+ | KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::CONTROL,
} => self.move_start(),
KeyEvent {
+ code: KeyCode::Left,
+ modifiers: KeyModifiers::ALT,
+ }
+ | KeyEvent {
+ code: KeyCode::Char('b'),
+ modifiers: KeyModifiers::ALT,
+ } => self.move_cursor(Movement::BackwardWord(1)),
+ KeyEvent {
+ code: KeyCode::Right,
+ modifiers: KeyModifiers::ALT,
+ }
+ | KeyEvent {
+ code: KeyCode::Char('f'),
+ modifiers: KeyModifiers::ALT,
+ } => self.move_cursor(Movement::ForwardWord(1)),
+ KeyEvent {
code: KeyCode::Char('w'),
modifiers: KeyModifiers::CONTROL,
} => self.delete_word_backwards(),
KeyEvent {
+ code: KeyCode::Char('k'),
+ modifiers: KeyModifiers::CONTROL,
+ } => self.kill_to_end_of_line(),
+ KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE,
} => {
@@ -363,7 +499,9 @@ impl Component for Prompt {
(
Some(Position::new(
area.y as usize + line,
- area.x as usize + self.prompt.len() + self.cursor,
+ area.x as usize
+ + self.prompt.len()
+ + UnicodeWidthStr::width(&self.line[..self.cursor]),
)),
CursorKind::Block,
)