diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 58124ed2c3645..8ee3928091094 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -260,10 +260,10 @@ where scope: "source.rust".to_string(), file_types: vec!["rs".to_string()], language_id: Lang::Rust, - highlight_config: OnceCell::new(), // roots: vec![], auto_format: false, + highlight_config: None, language_server: None, indent: Some(IndentationConfiguration { tab_width: 4, diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index ae058eb18ca83..973c1c0e1a18e 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -5,9 +5,10 @@ use std::{ borrow::Cow, cell::RefCell, collections::{HashMap, HashSet}, + convert::TryInto, fmt, path::{Path, PathBuf}, - sync::Arc, + sync::{Arc, Mutex}, }; use once_cell::sync::{Lazy, OnceCell}; @@ -35,9 +36,9 @@ pub struct LanguageConfiguration { // injection_regex // first_line_regex // - #[serde(skip)] - pub(crate) highlight_config: OnceCell>>, // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583 + #[serde(skip)] + pub highlight_config: Option>>, #[serde(skip_serializing_if = "Option::is_none")] pub language_server: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -144,34 +145,30 @@ 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(); + 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 indent_query(&self) -> Option<&IndentQuery> { @@ -190,8 +187,6 @@ impl LanguageConfiguration { } } -pub static LOADER: OnceCell = OnceCell::new(); - #[derive(Debug)] pub struct Loader { // highlight_names ? diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 3d04344119508..f142fd447833d 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,11 +1,14 @@ +use dirs_next::config_dir; +use helix_core::syntax; use helix_lsp::lsp; -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, ui}; use log::{error, info}; use std::{ + collections::HashMap, future::Future, io::{self, stdout, Stdout, Write}, path::PathBuf, @@ -13,7 +16,9 @@ use std::{ time::Duration, }; -use anyhow::Error; +use serde::{Deserialize, Serialize}; + +use anyhow::{Context, Error}; use crossterm::{ event::{Event, EventStream}, @@ -32,10 +37,17 @@ pub type LspCallback = pub type LspCallbacks = FuturesUnordered; pub type LspCallbackWrapper = Box; +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Configuration { + pub theme: Option, +} + pub struct Application { compositor: Compositor, editor: Editor, + theme_loader: Arc, + syn_loader: Arc, callbacks: LspCallbacks, } @@ -44,7 +56,44 @@ 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)); + + // 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 app_conf = Configuration { theme: None }; + let app_conf = std::fs::read(conf_dir.join("config.toml")) + .context("failed to read global configuration") + .and_then(|v| { + toml::from_slice(v.as_slice()).context("failed to deserialize config.toml") + }) + .unwrap_or_else(|_| app_conf); + + let theme = if let Some(theme) = &app_conf.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, + theme.scopes().to_vec(), + )); + + let mut editor = Editor::new(size, theme_loader.clone(), syn_loader.clone()); compositor.push(Box::new(ui::EditorView::new())); @@ -68,10 +117,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(), }; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 10d5e2646892d..a4548ef200b2f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1134,6 +1134,23 @@ mod cmd { _quit_all(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 list_themes(editor: &mut Editor, args: &[&str], event: PromptEvent) { + let themes = editor.theme_loader.as_ref().names(); + let status = format!("Available themes: {}", themes.join(", ")); + editor.set_status(status); + } + pub const COMMAND_LIST: &[Command] = &[ Command { name: "quit", @@ -1247,6 +1264,20 @@ mod cmd { fun: force_quit_all, completer: None, }, + Command { + name: "theme", + alias: None, + doc: "Change the theme of current view. Requires theme name as argument (:theme )", + fun: theme, + completer: None, + }, + Command { + name: "list-themes", + alias: None, + doc: "List available themes.", + fun: list_themes, + completer: None, + }, ]; @@ -2648,7 +2679,7 @@ pub 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)); } diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 9d4e1c5b10971..ba11624a8070e 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,4 +1,4 @@ -use helix_term::application::Application; +use helix_term::application::{self, Application}; use helix_term::args::Args; use std::path::PathBuf; diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 00ecce0349336..e7102d5573ce6 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 be113747de228..eee641b0fc0d8 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)); @@ -196,7 +203,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 +214,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 a1c4b40735e14..439edb8723c29 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -7,7 +7,7 @@ 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, }; @@ -175,7 +175,7 @@ impl Document { } // TODO: async fn? - pub fn load(path: PathBuf) -> Result { + pub fn load(path: PathBuf, config_loader: Option<&syntax::Loader>) -> Result { use std::{fs::File, io::BufReader}; let doc = if !path.exists() { @@ -195,6 +195,10 @@ impl Document { doc.set_path(&path)?; doc.detect_indent_style(); + if let Some(loader) = config_loader { + doc.detect_language(None, loader); + } + Ok(doc) } @@ -268,11 +272,18 @@ 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(); + pub fn detect_language( + &mut self, + theme: Option<&crate::Theme>, + config_loader: &syntax::Loader, + ) { + if let Some(path) = &self.path { + let language_config = config_loader.language_config_for_file_name(path); + let scopes = if let Some(theme) = theme { + theme.scopes() + } else { + config_loader.scopes() + }; self.set_language(language_config, scopes); } } @@ -410,9 +421,6 @@ impl Document { // and error out when document is saved self.path = Some(path); - // try detecting the language based on filepath - self.detect_language(); - Ok(()) } @@ -435,10 +443,9 @@ 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, config_loader: Arc) { + let language_config = config_loader.language_config_for_scope(scope); + let scopes = config_loader.scopes(); self.set_language(language_config, scopes); } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index ef0d8213f400e..fccbdfc24e7b6 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,7 +1,12 @@ -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 std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use slotmap::SlotMap; @@ -18,6 +23,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)>, } @@ -29,27 +37,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 @@ -60,8 +52,10 @@ impl Editor { documents: SlotMap::with_key(), count: None, register: RegisterSelection::default(), - theme, + theme: themes.default(), language_servers, + syn_loader: config_loader, + theme_loader: themes, status_msg: None, } } @@ -78,6 +72,27 @@ impl Editor { self.status_msg = Some((error, Severity::Error)); } + pub fn set_theme(&mut self, theme: Theme) { + let config_loader = self.syn_loader.as_ref(); + for (_, doc) in self.documents.iter_mut() { + doc.detect_language(Some(&theme), config_loader); + } + self._refresh(); + self.theme = theme; + } + + pub fn set_theme_from_name>(&mut self, theme: S) { + let theme = match self.theme_loader.load(theme.as_ref()) { + Ok(theme) => theme, + Err(e) => { + log::warn!("failed setting theme `{}` - {}", theme.as_ref(), e); + return; + } + }; + + self.set_theme(theme); + } + fn _refresh(&mut self) { for (view, _) in self.tree.views_mut() { let doc = &self.documents[view.doc]; @@ -161,7 +176,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.syn_loader))?; // try to find a language server based on the language name let language_server = doc diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index efb6a1af589a5..664faea62b3b7 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -1,6 +1,11 @@ -use std::collections::HashMap; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; +use anyhow::Context; use log::warn; +use once_cell::sync::Lazy; use serde::{Deserialize, Deserializer}; use toml::Value; @@ -86,7 +91,65 @@ pub use tui::style::{Color, Modifier, Style}; // } /// Color theme for syntax highlighting. -#[derive(Debug)] + +pub static DEFAULT_THEME: Lazy = Lazy::new(|| { + toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") +}); + +#[derive(Clone, Debug)] +pub struct Loader { + base_dir: PathBuf, +} +impl Loader { + /// Creates a new loader with base directory set to `$base_dir/themes` + pub fn new>(base_dir: P) -> Self { + Self { + base_dir: base_dir.as_ref().join("themes"), + } + } + + /// Loads a theme from base directory of this `Loader` + pub fn load(&self, name: &str) -> Result { + let path = self.base_dir.join(name); + let data = std::fs::read(&path)?; + toml::from_slice(data.as_slice()).context("Failed to deserialize theme") + } + + /// Lists all theme names available in `themes` directory + pub fn names(&self) -> Vec { + std::fs::read_dir(&self.base_dir) + .map(|entries| { + entries + .filter_map(|entry| { + if let Ok(entry) = entry { + let path = entry.path(); + if let Some(ext) = path.extension() { + if ext != "toml" { + return None; + } + return Some( + entry + .file_name() + .to_string_lossy() + .trim_end_matches(".toml") + .to_owned(), + ); + } + } + None + }) + .collect() + }) + .unwrap_or_default() + } + + /// Returns the default theme + pub fn default(&self) -> Theme { + DEFAULT_THEME.clone() + } +} + +#[derive(Clone, Debug)] pub struct Theme { scopes: Vec, styles: HashMap,