From 10f9f72232f5789323d689bf0f9cd359715770d6 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sat, 19 Jun 2021 23:59:19 +0900 Subject: Revert "Refactor key into helix-view" Did not use defaults when custom keymap was used This reverts commit ca806d4f852e934651132fc9570a6110e30f646d. --- helix-term/src/ui/editor.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'helix-term/src/ui') diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index d0eedad6..3dc43d3f 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}, 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, @@ -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; -- cgit v1.2.3-70-g09d2 From ce97a2f05fcddf81d8210ec6b25411f8fd7d867a Mon Sep 17 00:00:00 2001 From: wojciechkepka Date: Sat, 19 Jun 2021 13:26:52 +0200 Subject: Add ability to change theme on editor --- Cargo.lock | 7 +++ helix-core/Cargo.toml | 1 + helix-core/src/indent.rs | 37 +++++++-------- helix-core/src/lib.rs | 2 +- helix-core/src/syntax.rs | 101 +++++++++++++++++++++++----------------- helix-term/src/application.rs | 40 ++++++++++++++-- helix-term/src/ui/completion.rs | 43 ++++++++++------- helix-term/src/ui/markdown.rs | 31 ++++++------ helix-view/src/document.rs | 43 ++++++++++------- helix-view/src/editor.rs | 75 ++++++++++++++++++----------- 10 files changed, 240 insertions(+), 140 deletions(-) (limited to 'helix-term/src/ui') diff --git a/Cargo.lock b/Cargo.lock index 24c277e1..896f7bc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" +[[package]] +name = "arc-swap" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e906254e445520903e7fc9da4f709886c84ae4bc4ddaf0e093188d66df4dc820" + [[package]] name = "autocfg" version = "1.0.1" @@ -254,6 +260,7 @@ dependencies = [ name = "helix-core" version = "0.2.0" dependencies = [ + "arc-swap", "etcetera", "helix-syntax", "once_cell", diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 13ac35fb..346dc050 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -25,6 +25,7 @@ unicode-general-category = "0.4.0" # slab = "0.4.2" tree-sitter = "0.19" once_cell = "1.8" +arc-swap = "1" regex = "1" serde = { version = "1.0", features = ["derive"] } diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 58124ed2..8e0379e2 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -254,26 +254,23 @@ where Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader, }; use once_cell::sync::OnceCell; - let loader = Loader::new( - Configuration { - language: vec![LanguageConfiguration { - scope: "source.rust".to_string(), - file_types: vec!["rs".to_string()], - language_id: Lang::Rust, - highlight_config: OnceCell::new(), - // - roots: vec![], - auto_format: false, - language_server: None, - indent: Some(IndentationConfiguration { - tab_width: 4, - unit: String::from(" "), - }), - indent_query: OnceCell::new(), - }], - }, - Vec::new(), - ); + let loader = Loader::new(Configuration { + language: vec![LanguageConfiguration { + scope: "source.rust".to_string(), + file_types: vec!["rs".to_string()], + language_id: Lang::Rust, + highlight_config: OnceCell::new(), + // + roots: vec![], + auto_format: false, + language_server: None, + indent: Some(IndentationConfiguration { + tab_width: 4, + unit: String::from(" "), + }), + indent_query: OnceCell::new(), + }], + }); // set runtime path so we can find the queries let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 03741719..d669fa49 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -50,7 +50,7 @@ pub fn find_root(root: Option<&str>) -> Option { } #[cfg(not(embed_runtime))] -fn runtime_dir() -> std::path::PathBuf { +pub fn runtime_dir() -> std::path::PathBuf { if let Ok(dir) = std::env::var("HELIX_RUNTIME") { return dir.into(); } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index ae058eb1..78623fd6 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -1,6 +1,8 @@ use crate::{regex::Regex, Change, Rope, RopeSlice, Transaction}; pub use helix_syntax::{get_language, get_language_name, Lang}; +use arc_swap::ArcSwap; + use std::{ borrow::Cow, cell::RefCell, @@ -143,35 +145,48 @@ fn read_query(language: &str, filename: &str) -> String { } impl LanguageConfiguration { - pub fn highlight_config(&self, scopes: &[String]) -> Option> { - self.highlight_config - .get_or_init(|| { - let language = get_language_name(self.language_id).to_ascii_lowercase(); + fn initialize_highlight(&self, scopes: &[String]) -> Option> { + let language = get_language_name(self.language_id).to_ascii_lowercase(); - let highlights_query = read_query(&language, "highlights.scm"); - // always highlight syntax errors - // highlights_query += "\n(ERROR) @error"; + let highlights_query = read_query(&language, "highlights.scm"); + // always highlight syntax errors + // highlights_query += "\n(ERROR) @error"; - let injections_query = read_query(&language, "injections.scm"); + let injections_query = read_query(&language, "injections.scm"); - let locals_query = ""; + let locals_query = ""; - if highlights_query.is_empty() { - None - } else { - let language = get_language(self.language_id); - let mut config = HighlightConfiguration::new( - language, - &highlights_query, - &injections_query, - locals_query, - ) - .unwrap(); // TODO: no unwrap - config.configure(scopes); - Some(Arc::new(config)) - } - }) - .clone() + if highlights_query.is_empty() { + None + } else { + let language = get_language(self.language_id); + let mut config = HighlightConfiguration::new( + language, + &highlights_query, + &injections_query, + locals_query, + ) + .unwrap(); // TODO: no unwrap + config.configure(scopes); + Some(Arc::new(config)) + } + } + + pub fn highlight_config(&self, scopes: &[String]) -> Option> { + if let Some(config) = self.highlight_config.get() { + if let Some(config) = config { + config.configure(scopes); + } + config.clone() + } else { + self.highlight_config + .get_or_init(|| self.initialize_highlight(scopes)) + .clone() + } + } + + pub fn is_highlight_initialized(&self) -> bool { + self.highlight_config.get().is_some() } pub fn indent_query(&self) -> Option<&IndentQuery> { @@ -190,22 +205,18 @@ impl LanguageConfiguration { } } -pub static LOADER: OnceCell = OnceCell::new(); - #[derive(Debug)] pub struct Loader { // highlight_names ? language_configs: Vec>, language_config_ids_by_file_type: HashMap, // Vec - scopes: Vec, } impl Loader { - pub fn new(config: Configuration, scopes: Vec) -> Self { + pub fn new(config: Configuration) -> Self { let mut loader = Self { language_configs: Vec::new(), language_config_ids_by_file_type: HashMap::new(), - scopes, }; for config in config.language { @@ -225,10 +236,6 @@ impl Loader { loader } - pub fn scopes(&self) -> &[String] { - &self.scopes - } - pub fn language_config_for_file_name(&self, path: &Path) -> Option> { // Find all the language configurations that match this file name // or a suffix of the file name. @@ -253,6 +260,10 @@ impl Loader { .find(|config| config.scope == scope) .cloned() } + + pub fn language_configs_iter(&self) -> impl Iterator> { + self.language_configs.iter() + } } pub struct TsParser { @@ -771,7 +782,7 @@ pub struct HighlightConfiguration { combined_injections_query: Option, locals_pattern_index: usize, highlights_pattern_index: usize, - highlight_indices: Vec>, + highlight_indices: ArcSwap>>, non_local_variable_patterns: Vec, injection_content_capture_index: Option, injection_language_capture_index: Option, @@ -923,7 +934,7 @@ impl HighlightConfiguration { } } - let highlight_indices = vec![None; query.capture_names().len()]; + let highlight_indices = ArcSwap::from_pointee(vec![None; query.capture_names().len()]); Ok(Self { language, query, @@ -956,17 +967,20 @@ impl HighlightConfiguration { /// /// When highlighting, results are returned as `Highlight` values, which contain the index /// of the matched highlight this list of highlight names. - pub fn configure(&mut self, recognized_names: &[String]) { + pub fn configure(&self, recognized_names: &[String]) { let mut capture_parts = Vec::new(); - self.highlight_indices.clear(); - self.highlight_indices - .extend(self.query.capture_names().iter().map(move |capture_name| { + let indices: Vec<_> = self + .query + .capture_names() + .iter() + .map(move |capture_name| { capture_parts.clear(); capture_parts.extend(capture_name.split('.')); let mut best_index = None; let mut best_match_len = 0; for (i, recognized_name) in recognized_names.iter().enumerate() { + let recognized_name = recognized_name; let mut len = 0; let mut matches = true; for part in recognized_name.split('.') { @@ -982,7 +996,10 @@ impl HighlightConfiguration { } } best_index.map(Highlight) - })); + }) + .collect(); + + self.highlight_indices.store(Arc::new(indices)); } } @@ -1561,7 +1578,7 @@ where } } - let current_highlight = layer.config.highlight_indices[capture.index as usize]; + let current_highlight = layer.config.highlight_indices.load()[capture.index as usize]; // If this node represents a local definition, then store the current // highlight value on the local scope entry representing this node. diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 2fae467f..08853ed0 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,5 +1,6 @@ +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, keymap::Keymaps, ui}; @@ -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, + syn_loader: Arc, callbacks: LspCallbacks, lsp_progress: LspProgressMap, @@ -47,7 +50,34 @@ impl Application { use helix_view::editor::Action; let mut compositor = Compositor::new()?; let size = compositor.size(); - let mut editor = Editor::new(size); + + 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); @@ -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/ui/completion.rs b/helix-term/src/ui/completion.rs index 06ed966d..88a71534 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -246,34 +246,43 @@ 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, }; diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index be113747..91086f7b 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, } // 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) -> 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, }; @@ -196,7 +201,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 }) @@ -207,7 +212,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-view/src/document.rs b/helix-view/src/document.rs index e9a8097c..4d5a23b6 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -9,11 +9,11 @@ use std::sync::Arc; use helix_core::{ chars::{char_is_linebreak, char_is_whitespace}, history::History, - syntax::{LanguageConfiguration, LOADER}, + syntax::{self, LanguageConfiguration}, ChangeSet, Diagnostic, Rope, Selection, State, Syntax, Transaction, }; -use crate::{DocumentId, ViewId}; +use crate::{DocumentId, Theme, ViewId}; use std::collections::HashMap; @@ -236,7 +236,11 @@ impl Document { } // TODO: async fn? - pub fn load(path: PathBuf) -> Result { + pub fn load( + path: PathBuf, + theme: Option<&Theme>, + config_loader: Option<&syntax::Loader>, + ) -> Result { use std::{fs::File, io::BufReader}; let doc = if !path.exists() { @@ -256,6 +260,10 @@ impl Document { doc.set_path(&path)?; doc.detect_indent_style(); + if let Some(loader) = config_loader { + doc.detect_language(theme, loader); + } + Ok(doc) } @@ -330,12 +338,10 @@ impl Document { } } - fn detect_language(&mut self) { - if let Some(path) = self.path() { - let loader = LOADER.get().unwrap(); - let language_config = loader.language_config_for_file_name(path); - let scopes = loader.scopes(); - self.set_language(language_config, scopes); + pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) { + if let Some(path) = &self.path { + let language_config = config_loader.language_config_for_file_name(path); + self.set_language(theme, language_config); } } @@ -472,18 +478,16 @@ impl Document { // and error out when document is saved self.path = Some(path); - // try detecting the language based on filepath - self.detect_language(); - Ok(()) } pub fn set_language( &mut self, + theme: Option<&Theme>, language_config: Option>, - scopes: &[String], ) { if let Some(language_config) = language_config { + let scopes = theme.map(|theme| theme.scopes()).unwrap_or(&[]); if let Some(highlight_config) = language_config.highlight_config(scopes) { let syntax = Syntax::new(&self.text, highlight_config); self.syntax = Some(syntax); @@ -497,12 +501,15 @@ impl Document { }; } - pub fn set_language2(&mut self, scope: &str) { - let loader = LOADER.get().unwrap(); - let language_config = loader.language_config_for_scope(scope); - let scopes = loader.scopes(); + pub fn set_language2( + &mut self, + scope: &str, + theme: Option<&Theme>, + config_loader: Arc, + ) { + let language_config = config_loader.language_config_for_scope(scope); - self.set_language(language_config, scopes); + self.set_language(theme, language_config); } pub fn set_language_server(&mut self, language_server: Option>) { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index db8ae87a..83d5cbf6 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,10 +1,14 @@ -use crate::{theme::Theme, tree::Tree, Document, DocumentId, RegisterSelection, View, ViewId}; +use crate::{ + theme::{self, Theme}, + tree::Tree, + Document, DocumentId, RegisterSelection, View, ViewId, +}; +use helix_core::syntax; use tui::layout::Rect; use tui::terminal::CursorKind; use futures_util::future; -use std::path::PathBuf; -use std::time::Duration; +use std::{path::PathBuf, sync::Arc, time::Duration}; use slotmap::SlotMap; @@ -24,6 +28,9 @@ pub struct Editor { pub theme: Theme, pub language_servers: helix_lsp::Registry, + pub syn_loader: Arc, + pub theme_loader: Arc, + pub status_msg: Option<(String, Severity)>, } @@ -35,27 +42,11 @@ pub enum Action { } impl Editor { - pub fn new(mut area: tui::layout::Rect) -> Self { - use helix_core::config_dir; - let config = std::fs::read(config_dir().join("theme.toml")); - // load $HOME/.config/helix/theme.toml, fallback to default config - let toml = config - .as_deref() - .unwrap_or(include_bytes!("../../theme.toml")); - let theme: Theme = toml::from_slice(toml).expect("failed to parse theme.toml"); - - // initialize language registry - use helix_core::syntax::{Loader, LOADER}; - - // load $HOME/.config/helix/languages.toml, fallback to default config - let config = std::fs::read(helix_core::config_dir().join("languages.toml")); - let toml = config - .as_deref() - .unwrap_or(include_bytes!("../../languages.toml")); - - let config = toml::from_slice(toml).expect("Could not parse languages.toml"); - LOADER.get_or_init(|| Loader::new(config, theme.scopes().to_vec())); - + pub fn new( + mut area: tui::layout::Rect, + themes: Arc, + config_loader: Arc, + ) -> Self { let language_servers = helix_lsp::Registry::new(); // HAXX: offset the render area height by 1 to account for prompt/commandline @@ -66,8 +57,10 @@ impl Editor { documents: SlotMap::with_key(), count: None, selected_register: RegisterSelection::default(), - theme, + theme: themes.default(), language_servers, + syn_loader: config_loader, + theme_loader: themes, registers: Registers::default(), status_msg: None, } @@ -85,6 +78,32 @@ impl Editor { self.status_msg = Some((error, Severity::Error)); } + pub fn set_theme(&mut self, theme: Theme) { + let scopes = theme.scopes(); + for config in self + .syn_loader + .language_configs_iter() + .filter(|cfg| cfg.is_highlight_initialized()) + { + config.highlight_config(scopes); + } + + self.theme = theme; + self._refresh(); + } + + pub fn set_theme_from_name(&mut self, theme: &str) { + let theme = match self.theme_loader.load(theme.as_ref()) { + Ok(theme) => theme, + Err(e) => { + log::warn!("failed setting theme `{}` - {}", theme, e); + return; + } + }; + + self.set_theme(theme); + } + fn _refresh(&mut self) { for (view, _) in self.tree.views_mut() { let doc = &self.documents[view.doc]; @@ -168,7 +187,7 @@ impl Editor { let id = if let Some(id) = id { id } else { - let mut doc = Document::load(path)?; + let mut doc = Document::load(path, Some(&self.theme), Some(&self.syn_loader))?; // try to find a language server based on the language name let language_server = doc @@ -254,6 +273,10 @@ impl Editor { self.documents.iter().map(|(_id, doc)| doc) } + pub fn documents_mut(&mut self) -> impl Iterator { + self.documents.iter_mut().map(|(_id, doc)| doc) + } + // pub fn current_document(&self) -> Document { // let id = self.view().doc; // let doc = &mut editor.documents[id]; -- cgit v1.2.3-70-g09d2 From a2db161d5a38172c09469262d432f37e10ce10ce Mon Sep 17 00:00:00 2001 From: wojciechkepka Date: Sat, 19 Jun 2021 13:27:06 +0200 Subject: Add theme completer --- helix-term/src/ui/mod.rs | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) (limited to 'helix-term/src/ui') 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 { 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; + pub fn theme(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"), + )); + 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 { // 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. -- cgit v1.2.3-70-g09d2 From 2d629a880cddec4938750b13bcd9926631c1db94 Mon Sep 17 00:00:00 2001 From: wojciechkepka Date: Sat, 19 Jun 2021 21:33:20 +0200 Subject: Fix overflow --- helix-term/src/ui/editor.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'helix-term/src/ui') diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 3dc43d3f..7f0d06e9 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -129,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(); @@ -185,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; @@ -197,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; } -- cgit v1.2.3-70-g09d2 From 9275021497fc13938317b681319ba571c8d5f478 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 18 Jun 2021 17:31:31 +0900 Subject: ui: prompt: Better unicode support We copied over eval_movement from wezterm, that already solves most of our problems. self.cursor is now byte-based. --- Cargo.lock | 2 + helix-core/Cargo.toml | 4 +- helix-term/Cargo.toml | 3 + helix-term/src/ui/prompt.rs | 181 +++++++++++++++++++++++++++++++++----------- 4 files changed, 144 insertions(+), 46 deletions(-) (limited to 'helix-term/src/ui') diff --git a/Cargo.lock b/Cargo.lock index 896f7bc1..960d55f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,6 +329,8 @@ dependencies = [ "serde_json", "tokio", "toml", + "unicode-segmentation", + "unicode-width", ] [[package]] diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 346dc050..bab062e1 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -19,9 +19,9 @@ helix-syntax = { version = "0.2", path = "../helix-syntax" } ropey = "1.3" smallvec = "1.4" tendril = "0.4.2" -unicode-segmentation = "1.7.1" +unicode-segmentation = "1.7" unicode-width = "0.1" -unicode-general-category = "0.4.0" +unicode-general-category = "0.4" # slab = "0.4.2" tree-sitter = "0.19" once_cell = "1.8" diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 385af64c..24741796 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -54,3 +54,6 @@ toml = "0.5" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } + +unicode-segmentation = "1.7" +unicode-width = "0.1" diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 991b328d..d1413209 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -6,6 +6,9 @@ use helix_view::{Editor, Theme}; use std::{borrow::Cow, ops::RangeFrom}; use tui::terminal::CursorKind; +use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete}; +use unicode_width::UnicodeWidthStr; + pub type Completion = (RangeFrom, Cow<'static, str>); pub struct Prompt { @@ -34,6 +37,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 +66,125 @@ 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) + let pos = self.eval_movement(Movement::BackwardChar(1)); + self.cursor = pos } pub fn move_char_right(&mut self) { - if self.cursor < self.line.len() { - self.cursor += 1; - } + let pos = self.eval_movement(Movement::ForwardChar(1)); + self.cursor = pos; } pub fn move_start(&mut self) { @@ -87,39 +196,21 @@ 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..); - self.completion = (self.completion_fn)(&self.line); + 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 clear(&mut self) { @@ -363,7 +454,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, ) -- cgit v1.2.3-70-g09d2 From e9a3245aae0e4380201cffcff7ebe06c129823c5 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 18 Jun 2021 17:47:10 +0900 Subject: Re-export unicode crates from helix_core --- Cargo.lock | 2 -- helix-core/src/lib.rs | 8 ++++++-- helix-term/Cargo.toml | 3 --- helix-term/src/ui/prompt.rs | 6 ++++-- 4 files changed, 10 insertions(+), 9 deletions(-) (limited to 'helix-term/src/ui') diff --git a/Cargo.lock b/Cargo.lock index 960d55f1..896f7bc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,8 +329,6 @@ dependencies = [ "serde_json", "tokio", "toml", - "unicode-segmentation", - "unicode-width", ] [[package]] diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index d669fa49..4a9ac891 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -18,6 +18,12 @@ mod state; pub mod syntax; mod transaction; +pub mod unicode { + pub use unicode_general_category as category; + pub use unicode_segmentation as segmentation; + pub use unicode_width as width; +} + static RUNTIME_DIR: once_cell::sync::Lazy = once_cell::sync::Lazy::new(runtime_dir); @@ -97,8 +103,6 @@ pub use ropey::{Rope, RopeSlice}; pub use tendril::StrTendril as Tendril; -pub use unicode_general_category::get_general_category; - #[doc(inline)] pub use {regex, tree_sitter}; diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 24741796..385af64c 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -54,6 +54,3 @@ toml = "0.5" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } - -unicode-segmentation = "1.7" -unicode-width = "0.1" diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index d1413209..22158e78 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -6,8 +6,10 @@ use helix_view::{Editor, Theme}; use std::{borrow::Cow, ops::RangeFrom}; use tui::terminal::CursorKind; -use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete}; -use unicode_width::UnicodeWidthStr; +use helix_core::{ + unicode::segmentation::{GraphemeCursor, GraphemeIncomplete}, + unicode::width::UnicodeWidthStr, +}; pub type Completion = (RangeFrom, Cow<'static, str>); -- cgit v1.2.3-70-g09d2 From 34ebe8265468df755598dde2ec79c249b581ba97 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Sat, 19 Jun 2021 16:31:04 +0900 Subject: ui: prompt: Add more keymappings --- helix-term/src/ui/prompt.rs | 61 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 9 deletions(-) (limited to 'helix-term/src/ui') diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 22158e78..7ca4308c 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -179,16 +179,11 @@ impl Prompt { self.exit_selection(); } - pub fn move_char_left(&mut self) { - let pos = self.eval_movement(Movement::BackwardChar(1)); + pub fn move_cursor(&mut self, movement: Movement) { + let pos = self.eval_movement(movement); self.cursor = pos } - pub fn move_char_right(&mut self) { - let pos = self.eval_movement(Movement::ForwardChar(1)); - self.cursor = pos; - } - pub fn move_start(&mut self) { self.cursor = 0; } @@ -215,6 +210,14 @@ impl Prompt { 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) { self.line.clear(); self.cursor = 0; @@ -386,31 +389,71 @@ 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, -- cgit v1.2.3-70-g09d2 From 980e6023523119676652b49692eb5f844d84d703 Mon Sep 17 00:00:00 2001 From: wojciechkepka Date: Sun, 20 Jun 2021 06:13:44 +0200 Subject: Make completion window move to top when cursor is below half --- helix-term/src/ui/completion.rs | 13 +++++++++++-- helix-term/src/ui/markdown.rs | 1 - 2 files changed, 11 insertions(+), 3 deletions(-) (limited to 'helix-term/src/ui') diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 88a71534..f4d882de 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; let doc = match &option.documentation { Some(lsp::Documentation::String(contents)) @@ -289,8 +292,14 @@ impl Component for Completion { 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); + let y = if cursor_pos > half as usize { + 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/markdown.rs b/helix-term/src/ui/markdown.rs index 91086f7b..75e2f4b4 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -162,7 +162,6 @@ fn parse<'a>( } } Event::Code(text) | Event::Html(text) => { - log::warn!("code {:?}", text); let mut span = to_span(text); span.style = code_style; spans.push(span); -- cgit v1.2.3-70-g09d2 From 0882712b4598586ce7c9b8e8f446d6a6fc5ff060 Mon Sep 17 00:00:00 2001 From: wojciechkepka Date: Sun, 20 Jun 2021 07:15:08 +0200 Subject: Use full screen size --- helix-term/src/ui/completion.rs | 8 +++++--- helix-view/src/tree.rs | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) (limited to 'helix-term/src/ui') diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index f4d882de..256d8f7d 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -239,8 +239,8 @@ impl Component for Completion { .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; + 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)) @@ -292,7 +292,9 @@ impl Component for Completion { let half = area.height / 2; let height = 15.min(half); - let y = if cursor_pos > half as usize { + let y = if cursor_pos + view.area.y + >= (cx.editor.tree.area().height - height - 1/* statusline */) + { 0 } else { // -2 to subtract command line + statusline. a bit of a hack, because of splits. diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index a0c466d9..f7d6c1f2 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -434,6 +434,10 @@ impl Tree { self.focus = key; } } + + pub fn area(&self) -> Rect { + self.area + } } #[derive(Debug)] -- cgit v1.2.3-70-g09d2 From fc39a6c40df7d4a28994db762554815c37470073 Mon Sep 17 00:00:00 2001 From: wojciechkepka Date: Sun, 20 Jun 2021 12:35:12 +0200 Subject: Add comment, statusline + commandline = 2 --- helix-term/src/ui/completion.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'helix-term/src/ui') diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 256d8f7d..80f7d590 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -292,8 +292,9 @@ impl Component for Completion { let half = area.height / 2; let height = 15.min(half); + // 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 - 1/* statusline */) + >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */) { 0 } else { -- cgit v1.2.3-70-g09d2