Compare commits

..

2 Commits

Author SHA1 Message Date
RoloEdits 5ce27b0087
Merge 70deab1b19 into 4281228da3 2025-07-24 14:36:52 -03:00
Rolo 70deab1b19 feat: add support for basic icons 2025-07-23 03:18:31 -07:00
11 changed files with 176 additions and 237 deletions

View File

@ -3215,9 +3215,9 @@ fn buffer_picker(cx: &mut Context) {
spans.push(icon.to_span_with(|icon| format!("{icon} ")));
}
spans.push(Span::raw(name.to_string()));
spans.push(name.to_string().into());
Cell::from(Spans::from(spans))
Spans::from(spans).into()
}),
];
let picker = Picker::new(columns, 2, items, (), |cx, meta, action| {
@ -3281,7 +3281,6 @@ fn jumplist_picker(cx: &mut Context) {
.as_deref()
.and_then(Path::to_str)
.unwrap_or(SCRATCH_BUFFER_NAME);
let icons = ICONS.load();
let mut spans = Vec::with_capacity(2);
@ -3290,9 +3289,9 @@ fn jumplist_picker(cx: &mut Context) {
spans.push(icon.to_span_with(|icon| format!("{icon} ")));
}
spans.push(Span::raw(name.to_string()));
spans.push(name.to_string().into());
Cell::from(Spans::from(spans))
Spans::from(spans).into()
}),
ui::PickerColumn::new("flags", |item: &JumpMeta, _| {
let mut flags = Vec::new();

View File

@ -247,7 +247,6 @@ fn diag_picker(
"severity",
|item: &PickerDiagnostic, styles: &DiagnosticStyles| {
let icons = ICONS.load();
match item.diag.severity {
Some(DiagnosticSeverity::HINT) => {
Span::styled(format!("{} HINT", icons.diagnostic().hint()), styles.hint)
@ -416,9 +415,8 @@ pub fn symbol_picker(cx: &mut Context) {
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
let columns = [
ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
let name = display_symbol_kind(item.symbol.kind);
let icons = ICONS.load();
let name = display_symbol_kind(item.symbol.kind);
if let Some(icon) = icons.kind().get(name) {
icon.to_span_with(|icon| format!("{icon} {name}")).into()

View File

@ -107,8 +107,8 @@ impl menu::Item for CompletionItem {
};
let icons = ICONS.load();
let name = &kind.0[0].content;
let is_folder = kind.0[0].content == "folder";
if let Some(icon) = icons.kind().get(name) {

View File

@ -9,7 +9,6 @@ use helix_core::{visual_offset_from_block, Position, RopeSlice};
use helix_stdx::rope::RopeSliceExt;
use helix_view::editor::{WhitespaceConfig, WhitespaceRenderValue};
use helix_view::graphics::Rect;
use helix_view::icons::ICONS;
use helix_view::theme::Style;
use helix_view::view::ViewPosition;
use helix_view::{Document, Theme};
@ -207,41 +206,38 @@ impl<'a> TextRenderer<'a> {
viewport: Rect,
) -> TextRenderer<'a> {
let editor_config = doc.config.load();
let WhitespaceConfig { render } = &editor_config.whitespace;
let WhitespaceConfig {
render: ws_render,
characters: ws_chars,
} = &editor_config.whitespace;
let tab_width = doc.tab_width();
let icons = ICONS.load();
let whitespace = icons.ui().r#virtual();
let tab = if render.tab() == WhitespaceRenderValue::All {
std::iter::once(whitespace.tab())
.chain(std::iter::repeat(whitespace.tabpad()).take(tab_width - 1))
let tab = if ws_render.tab() == WhitespaceRenderValue::All {
std::iter::once(ws_chars.tab)
.chain(std::iter::repeat(ws_chars.tabpad).take(tab_width - 1))
.collect()
} else {
" ".repeat(tab_width)
};
let virtual_tab = " ".repeat(tab_width);
let newline = if render.newline() == WhitespaceRenderValue::All {
whitespace.newline().into()
let newline = if ws_render.newline() == WhitespaceRenderValue::All {
ws_chars.newline.into()
} else {
" ".to_owned()
};
let space = if render.space() == WhitespaceRenderValue::All {
whitespace.space().into()
let space = if ws_render.space() == WhitespaceRenderValue::All {
ws_chars.space.into()
} else {
" ".to_owned()
};
let nbsp = if render.nbsp() == WhitespaceRenderValue::All {
whitespace.nbsp().into()
let nbsp = if ws_render.nbsp() == WhitespaceRenderValue::All {
ws_chars.nbsp.into()
} else {
" ".to_owned()
};
let nnbsp = if render.nnbsp() == WhitespaceRenderValue::All {
whitespace.nnbsp().into()
let nnbsp = if ws_render.nnbsp() == WhitespaceRenderValue::All {
ws_chars.nnbsp.into()
} else {
" ".to_owned()
};
@ -275,7 +271,6 @@ impl<'a> TextRenderer<'a> {
offset,
}
}
/// Draws a single `grapheme` at the current render position with a specified `style`.
pub fn draw_decoration_grapheme(
&mut self,

View File

@ -235,8 +235,7 @@ impl EditorView {
theme: &Theme,
) {
let editor_rulers = &editor.config().rulers;
let style = theme
let ruler_theme = theme
.try_get("ui.virtual.ruler")
.unwrap_or_else(|| Style::default().bg(Color::Red));
@ -254,12 +253,7 @@ impl EditorView {
.filter_map(|ruler| ruler.checked_sub(1 + view_offset.horizontal_offset as u16))
.filter(|ruler| ruler < &viewport.width)
.map(|ruler| viewport.clip_left(ruler).with_width(1))
.for_each(|area| {
let icons = ICONS.load();
for y in area.y..area.height {
surface.set_string(area.x, y, icons.ui().r#virtual().ruler(), style);
}
});
.for_each(|area| surface.set_style(area, ruler_theme))
}
fn viewport_byte_range(
@ -589,7 +583,7 @@ impl EditorView {
let mut x = viewport.x;
let current_doc = view!(editor).doc;
for doc in editor.documents() {
for (idx, doc) in editor.documents().enumerate() {
let fname = doc
.path()
.unwrap_or(&scratch)
@ -604,9 +598,24 @@ impl EditorView {
bufferline_inactive
};
let lang = doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME);
let icons = ICONS.load();
let lang = doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME);
// Render the separator before the text if the current document is not first.
if idx > 0 {
let used_width = viewport.x.saturating_sub(x);
let rem_width = surface.area.width.saturating_sub(used_width);
x = surface
.set_stringn(
x,
viewport.y,
icons.ui().bufferline().separator(),
rem_width as usize,
bufferline_inactive,
)
.0;
}
if let Some(icon) = icons
.fs()

View File

@ -32,7 +32,6 @@ pub use text::Text;
use helix_view::Editor;
use tui::text::{Span, Spans, ToSpan};
use tui::widgets::Cell;
use std::path::Path;
use std::{error::Error, path::PathBuf};
@ -334,7 +333,7 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result<FileExplorer, std
spans.push(icon.to_span_with(|icon| format!("{icon} ")));
spans.push(Span::raw(name));
Cell::from(Spans::from(spans))
Spans::from(spans).into()
} else {
name.into()
}

View File

@ -236,7 +236,6 @@ where
});
let icons = ICONS.load();
for sev in &context.editor.config().statusline.diagnostics {
match sev {
Severity::Hint if hints > 0 => {
@ -487,10 +486,10 @@ fn render_file_type<'a, F>(context: &mut RenderContext<'a>, write: F)
where
F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy,
{
let icons = ICONS.load();
let lang = context.doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME);
let icons = ICONS.load();
if let Some(icon) = icons
.fs()
.from_optional_path_or_lang(context.doc.path().map(|path| path.as_path()), lang)
@ -578,13 +577,10 @@ fn render_separator<'a, F>(context: &mut RenderContext<'a>, write: F)
where
F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy,
{
let sep = &context.editor.config().statusline.separator;
let style = context.editor.theme.get("ui.statusline.separator");
let icons = ICONS.load();
let separator = icons.ui().statusline().separator().to_string();
write(context, Span::styled(separator, style));
write(context, Span::styled(sep.to_string(), style));
}
fn render_spacer<'a, F>(context: &mut RenderContext<'a>, write: F)

View File

@ -43,7 +43,6 @@ use helix_core::{
ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction,
};
use crate::icons::{Icons, ICONS};
use crate::{
editor::Config,
events::{DocumentDidChange, SelectionDidChange},
@ -2244,10 +2243,8 @@ impl Document {
.unwrap_or(40);
let wrap_indicator = language_soft_wrap
.and_then(|soft_wrap| soft_wrap.wrap_indicator.clone())
.unwrap_or_else(|| {
let icons: arc_swap::access::DynGuard<Icons> = ICONS.load();
icons.ui().r#virtual().wrap().to_string()
});
.or_else(|| config.soft_wrap.wrap_indicator.clone())
.unwrap_or_else(|| "".into());
let tab_width = self.tab_width() as u16;
TextFormat {
soft_wrap: enable_soft_wrap && viewport_width > 10,

View File

@ -508,6 +508,7 @@ pub struct StatusLineConfig {
pub left: Vec<StatusLineElement>,
pub center: Vec<StatusLineElement>,
pub right: Vec<StatusLineElement>,
pub separator: String,
pub mode: ModeConfig,
pub diagnostics: Vec<Severity>,
pub workspace_diagnostics: Vec<Severity>,
@ -533,6 +534,7 @@ impl Default for StatusLineConfig {
E::Position,
E::FileEncoding,
],
separator: String::from(""),
mode: ModeConfig::default(),
diagnostics: vec![Severity::Warning, Severity::Error],
workspace_diagnostics: vec![Severity::Warning, Severity::Error],
@ -751,12 +753,14 @@ impl std::str::FromStr for GutterType {
#[serde(default)]
pub struct WhitespaceConfig {
pub render: WhitespaceRender,
pub characters: WhitespaceCharacters,
}
impl Default for WhitespaceConfig {
fn default() -> Self {
Self {
render: WhitespaceRender::Basic(WhitespaceRenderValue::None),
characters: WhitespaceCharacters::default(),
}
}
}
@ -882,6 +886,30 @@ where
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct WhitespaceCharacters {
pub space: char,
pub nbsp: char,
pub nnbsp: char,
pub tab: char,
pub tabpad: char,
pub newline: char,
}
impl Default for WhitespaceCharacters {
fn default() -> Self {
Self {
space: '·', // U+00B7
nbsp: '', // U+237D
nnbsp: '', // U+2423
tab: '', // U+2192
newline: '', // U+23CE
tabpad: ' ',
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct IndentGuidesConfig {

View File

@ -127,18 +127,17 @@ pub fn diff<'doc>(
let icons = ICONS.load();
let (icon, style) = if hunk.is_pure_insertion() {
(icons.ui().gutter().added(), added)
(icons.gutter().added(), added)
} else if hunk.is_pure_removal() {
if !first_visual_line {
return None;
}
(icons.ui().gutter().removed(), deleted)
(icons.gutter().removed(), deleted)
} else {
(icons.ui().gutter().modified(), modified)
(icons.gutter().modified(), modified)
};
write!(out, "{icon}").unwrap();
write!(out, "{}", icon).unwrap();
Some(style)
},
)

View File

@ -20,6 +20,7 @@ pub struct Icons {
diagnostic: Diagnostic,
vcs: Vcs,
dap: Dap,
gutter: Gutter,
ui: Ui,
}
@ -49,6 +50,11 @@ impl Icons {
&self.dap
}
#[inline]
pub fn gutter(&self) -> &Gutter {
&self.gutter
}
#[inline]
pub fn ui(&self) -> &Ui {
&self.ui
@ -107,46 +113,48 @@ impl Kind {
return None;
}
match kind {
"file" => self.file(),
"folder" => self.folder(),
"module" => self.module(),
"namespace" => self.namespace(),
"package" => self.package(),
"class" => self.class(),
"method" => self.method(),
"property" => self.property(),
"field" => self.field(),
"construct" => self.constructor(),
"enum" => self.r#enum(),
"interface" => self.interface(),
"function" => self.function(),
"variable" => self.variable(),
"constant" => self.constant(),
"string" => self.string(),
"number" => self.number(),
"boolean" => self.boolean(),
"array" => self.array(),
"object" => self.object(),
"key" => self.key(),
"null" => self.null(),
"enum_member" => self.enum_member(),
"struct" => self.r#struct(),
"event" => self.event(),
"operator" => self.operator(),
"typeparam" => self.type_parameter(),
"color" => Some(self.color()),
"keyword" => self.keyword(),
"value" => self.value(),
"snippet" => self.snippet(),
"reference" => self.reference(),
"text" => self.text(),
"unit" => self.unit(),
"word" => self.word(),
"spellcheck" => self.spellcheck(),
let icon = match kind {
"file" => self.file()?,
"folder" => self.folder()?,
"module" => self.module()?,
"namespace" => self.namespace()?,
"package" => self.package()?,
"class" => self.class()?,
"method" => self.method()?,
"property" => self.property()?,
"field" => self.field()?,
"construct" => self.constructor()?,
"enum" => self.r#enum()?,
"interface" => self.interface()?,
"function" => self.function()?,
"variable" => self.variable()?,
"constant" => self.constant()?,
"string" => self.string()?,
"number" => self.number()?,
"boolean" => self.boolean()?,
"array" => self.array()?,
"object" => self.object()?,
"key" => self.key()?,
"null" => self.null()?,
"enum_member" => self.enum_member()?,
"struct" => self.r#struct()?,
"event" => self.event()?,
"operator" => self.operator()?,
"typeparam" => self.type_parameter()?,
"color" => self.color(),
"keyword" => self.keyword()?,
"value" => self.value()?,
"snippet" => self.snippet()?,
"reference" => self.reference()?,
"text" => self.text()?,
"unit" => self.unit()?,
"word" => self.word()?,
"spellcheck" => self.spellcheck()?,
_ => None,
}
_ => return None,
};
Some(icon)
}
#[inline]
@ -548,23 +556,21 @@ pub struct Fs {
mime: HashMap<String, Icon>,
}
macro_rules! iconmap {
macro_rules! mimes {
( $( $key:literal => { glyph: $glyph:expr $(, color: $color:expr)? } ),* $(,)? ) => {{
HashMap::from(
[
$(
(String::from($key), Icon {
glyph: String::from($glyph),
color: None $(.or( Some(Color::from_hex($color).unwrap())) )?,
}),
)*
]
)
let mut map = HashMap::new();
$(
map.insert(String::from($key), Icon {
glyph: String::from($glyph),
color: None $(.or( Some(Color::from_hex($color).unwrap())) )?,
});
)*
map
}};
}
static MIMES: once_cell::sync::Lazy<HashMap<String, Icon>> = once_cell::sync::Lazy::new(|| {
iconmap! {
mimes! {
// Language name
"git-commit" => {glyph: "", color: "#f15233" },
"git-rebase" => {glyph: "", color: "#f15233" },
@ -689,11 +695,13 @@ impl Fs {
return None;
}
if is_open {
self.directory_open.as_deref().or(Some("󰝰"))
let dir = if is_open {
self.directory_open.as_deref().unwrap_or("󰝰")
} else {
self.directory.as_deref().or(Some("󰉋"))
}
self.directory.as_deref().unwrap_or("󰉋")
};
Some(dir)
}
#[inline]
@ -800,41 +808,6 @@ impl Dap {
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
#[serde(default, deny_unknown_fields)]
pub struct Ui {
workspace: Option<Icon>,
gutter: Gutter,
#[serde(rename = "virtual")]
r#virtual: Virtual,
statusline: Statusline,
}
impl Ui {
/// Returns a workspace diagnostic icon.
///
/// If no icon is set in the config, it will return `W` by default.
#[inline]
pub fn workspace(&self) -> Icon {
self.workspace.clone().unwrap_or_else(|| Icon::from("W"))
}
#[inline]
pub fn gutter(&self) -> &Gutter {
&self.gutter
}
#[inline]
pub fn r#virtual(&self) -> &Virtual {
&self.r#virtual
}
#[inline]
pub fn statusline(&self) -> &Statusline {
&self.statusline
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
pub struct Gutter {
added: Option<String>,
@ -859,100 +832,46 @@ impl Gutter {
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
pub struct Virtual {
// Whitespace
space: Option<String>,
nbsp: Option<String>,
nnbsp: Option<String>,
tab: Option<String>,
newline: Option<String>,
tabpad: Option<String>,
// Soft-wrap
wrap: Option<String>,
// Indentation guide
indentation: Option<String>,
// Ruler
ruler: Option<String>,
}
impl Virtual {
#[inline]
pub fn space(&self) -> &str {
// Default: U+00B7
self.space.as_deref().unwrap_or("·")
}
#[inline]
pub fn nbsp(&self) -> &str {
// Default: U+237D
self.nbsp.as_deref().unwrap_or("")
}
#[inline]
pub fn nnbsp(&self) -> &str {
// Default: U+2423
self.nnbsp.as_deref().unwrap_or("")
}
#[inline]
pub fn tab(&self) -> &str {
// Default: U+2192
self.tab.as_deref().unwrap_or("")
}
#[inline]
pub fn newline(&self) -> &str {
// Default: U+23CE
self.newline.as_deref().unwrap_or("")
}
#[inline]
pub fn tabpad(&self) -> &str {
// Default: U+23CE
self.tabpad.as_deref().unwrap_or(" ")
}
#[inline]
pub fn wrap(&self) -> &str {
// Default: U+21AA
self.wrap.as_deref().unwrap_or("")
}
#[inline]
pub fn indentation(&self) -> &str {
// Default: U+254E
self.indentation.as_deref().unwrap_or("")
}
#[inline]
pub fn ruler(&self) -> &str {
// TODO: Default: U+00A6: ¦
self.ruler.as_deref().unwrap_or(" ")
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
pub struct Statusline {
separator: Option<String>,
}
impl Statusline {
#[inline]
pub fn separator(&self) -> &str {
self.separator.as_deref().unwrap_or("")
}
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct Icon {
glyph: String,
color: Option<Color>,
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
#[serde(default, deny_unknown_fields)]
pub struct Ui {
workspace: Option<Icon>,
bufferline: BufferLine,
}
impl Ui {
/// Returns a workspace diagnostic icon.
///
/// If no icon is set in the config, it will return `W` by default.
#[inline]
pub fn workspace(&self) -> Icon {
self.workspace.clone().unwrap_or_else(|| Icon::from("W"))
}
#[inline]
pub fn bufferline(&self) -> &BufferLine {
&self.bufferline
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Clone, Default)]
pub struct BufferLine {
separator: Option<String>,
}
impl BufferLine {
#[inline]
pub fn separator(&self) -> &str {
self.separator.as_deref().unwrap_or("")
}
}
impl Icon {
pub fn glyph(&self) -> &str {
self.glyph.as_str()