From 48a1d675fa24859aae0d5d1599e617fabc31efa1 Mon Sep 17 00:00:00 2001 From: LazyTanuki <43273245+lazytanuki@users.noreply.github.com> Date: Tue, 21 Jun 2022 13:33:47 +0200 Subject: [PATCH] wip: cloned theme loader as the icons loader and display the filetype icon in the file picker --- helix-loader/src/config.rs | 27 +++++++++ helix-loader/src/lib.rs | 4 ++ helix-term/src/application.rs | 24 +++++++- helix-term/src/commands.rs | 4 +- helix-term/src/config.rs | 2 + helix-term/src/health.rs | 6 ++ helix-term/src/ui/mod.rs | 18 +++++- helix-view/src/editor.rs | 14 +++++ helix-view/src/icons.rs | 107 ++++++++++++++++++++++++++++++++++ helix-view/src/lib.rs | 1 + icons.toml | 13 +++++ 11 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 helix-view/src/icons.rs create mode 100644 icons.toml diff --git a/helix-loader/src/config.rs b/helix-loader/src/config.rs index a8c843612811c..c1c59bda51442 100644 --- a/helix-loader/src/config.rs +++ b/helix-loader/src/config.rs @@ -24,3 +24,30 @@ pub fn user_lang_config() -> Result { Ok(config) } + +/// Default built-in icons.toml. +pub fn default_icons_config() -> toml::Value { + toml::from_slice(include_bytes!("../../icons.toml")) + .expect("Could not parse built-in icons.toml to valid toml") +} + +/// User configured icons.toml file, merged with the default config. +pub fn user_icons_config() -> Result { + let config = crate::local_config_dirs() + .into_iter() + .chain([crate::config_dir()].into_iter()) + .map(|path| path.join("icons.toml")) + .filter_map(|file| { + std::fs::read(&file) + .map(|config| toml::from_slice(&config)) + .ok() + }) + .collect::, _>>()? + .into_iter() + .chain([default_icons_config()].into_iter()) + .fold(toml::Value::Table(toml::value::Table::default()), |a, b| { + crate::merge_toml_values(b, a, true) + }); + + Ok(config) +} diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index ff4414b2647d2..673479776bf00 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -68,6 +68,10 @@ pub fn log_file() -> std::path::PathBuf { cache_dir().join("helix.log") } +pub fn icons_config_file() -> std::path::PathBuf { + config_dir().join("icons.toml") +} + pub fn find_root_impl(root: Option<&str>, root_markers: &[String]) -> Vec { let current_dir = std::env::current_dir().expect("unable to determine current directory"); let mut directories = Vec::new(); diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 48e9c2758737c..4e72a84826674 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -5,7 +5,7 @@ use helix_core::{ pos_at_coords, syntax, Selection, }; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; -use helix_view::{align_view, editor::ConfigEvent, theme, Align, Editor}; +use helix_view::{align_view, editor::ConfigEvent, icons, theme, Align, Editor}; use serde_json::json; use crate::{ @@ -116,6 +116,24 @@ impl Application { } }); + let icons_loader = std::sync::Arc::new(icons::Loader::new( + &config_dir, + &helix_loader::runtime_dir(), + )); + let icons = config + .icons + .as_ref() + .and_then(|icons| { + icons_loader + .load(icons) + .map_err(|e| { + log::warn!("failed to load icons `{}` - {}", icons, e); + e + }) + .ok() + }) + .unwrap_or_else(|| icons_loader.default()); + let syn_loader_conf = user_syntax_loader().unwrap_or_else(|err| { eprintln!("Bad language config: {}", err); eprintln!("Press to continue with default language config"); @@ -131,6 +149,7 @@ impl Application { let mut editor = Editor::new( compositor.size(), theme_loader.clone(), + icons_loader.clone(), syn_loader.clone(), Box::new(Map::new(Arc::clone(&config), |config: &Config| { &config.editor @@ -153,7 +172,7 @@ impl Application { if first.is_dir() { std::env::set_current_dir(&first).context("set current dir")?; editor.new_file(Action::VerticalSplit); - let picker = ui::file_picker(".".into(), &config.load().editor); + let picker = ui::file_picker(".".into(), &config.load().editor, &icons); compositor.push(Box::new(overlayed(picker))); } else { let nr_of_files = args.files.len(); @@ -194,6 +213,7 @@ impl Application { } editor.set_theme(theme); + editor.set_icons(icons); #[cfg(windows)] let signals = futures_util::stream::empty(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a07b51091b8c2..c067b299aa842 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2148,13 +2148,13 @@ fn append_mode(cx: &mut Context) { fn file_picker(cx: &mut Context) { // We don't specify language markers, root will be the root of the current git repo let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./")); - let picker = ui::file_picker(root, &cx.editor.config()); + let picker = ui::file_picker(root, &cx.editor.config(), &cx.editor.icons); cx.push_layer(Box::new(overlayed(picker))); } fn file_picker_in_current_directory(cx: &mut Context) { let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("./")); - let picker = ui::file_picker(cwd, &cx.editor.config()); + let picker = ui::file_picker(cwd, &cx.editor.config(), &cx.editor.icons); cx.push_layer(Box::new(overlayed(picker))); } diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index 4407a882f8388..6812780c7c993 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -11,6 +11,7 @@ use toml::de::Error as TomlError; #[serde(deny_unknown_fields)] pub struct Config { pub theme: Option, + pub icons: Option, #[serde(default = "default")] pub keys: HashMap, #[serde(default)] @@ -21,6 +22,7 @@ impl Default for Config { fn default() -> Config { Config { theme: None, + icons: None, keys: default(), editor: helix_view::editor::Config::default(), } diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index f64e121d5fd17..c33e7fe6af8c7 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -52,6 +52,7 @@ pub fn general() -> std::io::Result<()> { let lang_file = helix_loader::lang_config_file(); let log_file = helix_loader::log_file(); let rt_dir = helix_loader::runtime_dir(); + let icons_file = helix_loader::icons_config_file(); if config_file.exists() { writeln!(stdout, "Config file: {}", config_file.display())?; @@ -63,6 +64,11 @@ pub fn general() -> std::io::Result<()> { } else { writeln!(stdout, "Language file: default")?; } + if icons_file.exists() { + writeln!(stdout, "Icons file: {}", icons_file.display())?; + } else { + writeln!(stdout, "Icons file: default")?; + } writeln!(stdout, "Log file: {}", log_file.display())?; writeln!(stdout, "Runtime directory: {}", rt_dir.display())?; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 47a68a18fa558..094d44aeec04a 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -12,6 +12,7 @@ mod text; pub use completion::Completion; pub use editor::EditorView; +use helix_view::icons::Icons; pub use markdown::Markdown; pub use menu::Menu; pub use picker::{FileLocation, FilePicker, Picker}; @@ -107,7 +108,11 @@ pub fn regex_prompt( cx.push_layer(Box::new(prompt)); } -pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker { +pub fn file_picker( + root: PathBuf, + config: &helix_view::editor::Config, + icons: &Icons, +) -> FilePicker { use ignore::{types::TypesBuilder, WalkBuilder}; use std::time::Instant; @@ -166,13 +171,22 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi files.take(MAX).collect() }; + let filetype_icons_enabled = config.file_picker.filetype_icons; + let icons = icons.clone(); + log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); FilePicker::new( files, move |path: &PathBuf| { // format_fn - path.strip_prefix(&root).unwrap_or(path).to_string_lossy() + let stripped_path = path.strip_prefix(&root).unwrap_or(path).to_string_lossy(); + if filetype_icons_enabled { + if let Some(icon) = icons.mimetype_icon_for_path(path) { + return format!("{} {}", icon, stripped_path).into(); + } + } + stripped_path }, move |cx, path: &PathBuf, action| { if let Err(e) = cx.editor.open(path, action) { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index c5a458d7f7db5..ddd080d70b1c6 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -2,6 +2,7 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, document::{Mode, SCRATCH_BUFFER_NAME}, graphics::{CursorKind, Rect}, + icons::{self, Icons}, info::Info, input::KeyEvent, theme::{self, Theme}, @@ -91,6 +92,8 @@ pub struct FilePickerConfig { /// WalkBuilder options /// Maximum Depth to recurse directories in file picker and global search. Defaults to `None`. pub max_depth: Option, + /// Enables filetype icons. + pub filetype_icons: bool, } impl Default for FilePickerConfig { @@ -104,6 +107,7 @@ impl Default for FilePickerConfig { git_global: true, git_exclude: true, max_depth: None, + filetype_icons: true, } } } @@ -458,6 +462,7 @@ pub struct Editor { pub macro_recording: Option<(char, Vec)>, pub macro_replaying: Vec, pub theme: Theme, + pub icons: Icons, pub language_servers: helix_lsp::Registry, pub debugger: Option, @@ -468,6 +473,7 @@ pub struct Editor { pub syn_loader: Arc, pub theme_loader: Arc, + pub icons_loader: Arc, pub status_msg: Option<(Cow<'static, str>, Severity)>, pub autoinfo: Option, @@ -510,6 +516,7 @@ impl Editor { pub fn new( mut area: Rect, theme_loader: Arc, + icons_loader: Arc, syn_loader: Arc, config: Box>, ) -> Self { @@ -529,12 +536,14 @@ impl Editor { macro_recording: None, macro_replaying: Vec::new(), theme: theme_loader.default(), + icons: icons_loader.default(), language_servers, debugger: None, debugger_events: SelectAll::new(), breakpoints: HashMap::new(), syn_loader, theme_loader, + icons_loader, registers: Registers::default(), clipboard_provider: get_clipboard_provider(), status_msg: None, @@ -618,6 +627,11 @@ impl Editor { self._refresh(); } + pub fn set_icons(&mut self, icons: Icons) { + self.icons = icons; + self._refresh(); + } + /// Refreshes the language server for a given document pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> { let doc = self.documents.get_mut(&doc_id)?; diff --git a/helix-view/src/icons.rs b/helix-view/src/icons.rs new file mode 100644 index 0000000000000..78d5ea55a9b20 --- /dev/null +++ b/helix-view/src/icons.rs @@ -0,0 +1,107 @@ +use anyhow::Context; +use once_cell::sync::Lazy; +use serde::Deserialize; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct Diagnostic { + pub error: char, + pub warning: char, + pub info: char, + pub notice: char, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct SymbolKind { + pub variable: char, + pub function: char, +} + +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub struct Icons { + mime_type: HashMap, + pub diagnostic: Diagnostic, + pub symbol_kind: SymbolKind, +} + +pub struct Loader { + user_dir: PathBuf, + default_dir: PathBuf, +} + +pub static DEFAULT_ICONS: Lazy = Lazy::new(|| { + toml::from_slice(include_bytes!("../../icons.toml")).expect("Failed to parse default icons") +}); + +impl Icons { + pub fn mimetype_icon_for_path(&self, path: &Path) -> Option<&char> { + if let Some(extension) = path.extension().and_then(|e| e.to_str()) { + self.mime_type.get(extension) + } else { + if let Some(filename) = path.file_name().and_then(|f| f.to_str()) { + self.mime_type.get(filename) + } else { + None + } + } + } +} + +impl Loader { + /// Creates a new loader that can load icons flavors from two directories. + pub fn new>(user_dir: P, default_dir: P) -> Self { + Self { + user_dir: user_dir.as_ref().join("icons"), + default_dir: default_dir.as_ref().join("icons"), + } + } + + /// Loads icons flavors first looking in the `user_dir` then in `default_dir` + pub fn load(&self, name: &str) -> Result { + if name == "default" { + return Ok(self.default()); + } + let filename = format!("{}.toml", name); + + let user_path = self.user_dir.join(&filename); + let path = if user_path.exists() { + user_path + } else { + self.default_dir.join(filename) + }; + + let data = std::fs::read(&path)?; + toml::from_slice(data.as_slice()).context("Failed to deserialize icon") + } + + pub fn read_names(path: &Path) -> Vec { + std::fs::read_dir(path) + .map(|entries| { + entries + .filter_map(|entry| { + let entry = entry.ok()?; + let path = entry.path(); + (path.extension()? == "toml") + .then(|| path.file_stem().unwrap().to_string_lossy().into_owned()) + }) + .collect() + }) + .unwrap_or_default() + } + + /// Lists all icons flavors names available in default and user directory + pub fn names(&self) -> Vec { + let mut names = Self::read_names(&self.user_dir); + names.extend(Self::read_names(&self.default_dir)); + names + } + + /// Returns the default icon flavor + pub fn default(&self) -> Icons { + DEFAULT_ICONS.clone() + } +} diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 788304bce3608..b78d64e55bd4b 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -10,6 +10,7 @@ pub mod handlers { pub mod dap; pub mod lsp; } +pub mod icons; pub mod info; pub mod input; pub mod keyboard; diff --git a/icons.toml b/icons.toml new file mode 100644 index 0000000000000..9f825bfb4ca78 --- /dev/null +++ b/icons.toml @@ -0,0 +1,13 @@ +[diagnostic] +error = "e" +warning = "w" +info = "i" +notice = "n" + +[symbol-kind] +variable = "v" +function = "f" + +[mime-type] +txt = "🎉" +rs = ""