mirror of https://github.com/helix-editor/helix
Merge 9f34b62a1e
into 4281228da3
commit
27d8bd20bf
|
@ -1644,6 +1644,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"slotmap",
|
||||
"smartstring",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
|
@ -2577,6 +2578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
|
|
@ -14,7 +14,7 @@ use helix_vcs::{FileChange, Hunk};
|
|||
pub use lsp::*;
|
||||
pub use syntax::*;
|
||||
use tui::{
|
||||
text::{Span, Spans},
|
||||
text::{Span, Spans, ToSpan},
|
||||
widgets::Cell,
|
||||
};
|
||||
pub use typed::*;
|
||||
|
@ -46,6 +46,7 @@ use helix_core::{
|
|||
use helix_view::{
|
||||
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
|
||||
editor::Action,
|
||||
icons::ICONS,
|
||||
info::Info,
|
||||
input::KeyEvent,
|
||||
keyboard::KeyCode,
|
||||
|
@ -2496,12 +2497,22 @@ fn global_search(cx: &mut Context) {
|
|||
.expect("global search paths are normalized (can't end in `..`)")
|
||||
.to_string_lossy();
|
||||
|
||||
Cell::from(Spans::from(vec![
|
||||
let mut spans = Vec::with_capacity(5);
|
||||
|
||||
let icons = ICONS.load();
|
||||
|
||||
if let Some(icon) = icons.fs().from_path(&path) {
|
||||
spans.push(icon.to_span_with(|icon| format!("{icon} ")));
|
||||
}
|
||||
|
||||
spans.extend_from_slice(&[
|
||||
Span::styled(directories, config.directory_style),
|
||||
Span::raw(filename),
|
||||
Span::styled(":", config.colon_style),
|
||||
Span::styled((item.line_num + 1).to_string(), config.number_style),
|
||||
]))
|
||||
]);
|
||||
|
||||
Cell::from(Spans::from(spans))
|
||||
}),
|
||||
PickerColumn::hidden("contents"),
|
||||
];
|
||||
|
@ -3190,11 +3201,23 @@ fn buffer_picker(cx: &mut Context) {
|
|||
.path
|
||||
.as_deref()
|
||||
.map(helix_stdx::path::get_relative_path);
|
||||
path.as_deref()
|
||||
|
||||
let name = path
|
||||
.as_deref()
|
||||
.and_then(Path::to_str)
|
||||
.unwrap_or(SCRATCH_BUFFER_NAME)
|
||||
.to_string()
|
||||
.into()
|
||||
.unwrap_or(SCRATCH_BUFFER_NAME);
|
||||
|
||||
let icons = ICONS.load();
|
||||
|
||||
let mut spans = Vec::with_capacity(2);
|
||||
|
||||
if let Some(icon) = icons.fs().from_optional_path(path.as_deref()) {
|
||||
spans.push(icon.to_span_with(|icon| format!("{icon} ")));
|
||||
}
|
||||
|
||||
spans.push(Span::raw(name.to_string()));
|
||||
|
||||
Cell::from(Spans::from(spans))
|
||||
}),
|
||||
];
|
||||
let picker = Picker::new(columns, 2, items, (), |cx, meta, action| {
|
||||
|
@ -3253,11 +3276,23 @@ fn jumplist_picker(cx: &mut Context) {
|
|||
.path
|
||||
.as_deref()
|
||||
.map(helix_stdx::path::get_relative_path);
|
||||
path.as_deref()
|
||||
|
||||
let name = path
|
||||
.as_deref()
|
||||
.and_then(Path::to_str)
|
||||
.unwrap_or(SCRATCH_BUFFER_NAME)
|
||||
.to_string()
|
||||
.into()
|
||||
.unwrap_or(SCRATCH_BUFFER_NAME);
|
||||
|
||||
let icons = ICONS.load();
|
||||
|
||||
let mut spans = Vec::with_capacity(2);
|
||||
|
||||
if let Some(icon) = icons.fs().from_optional_path(path.as_deref()) {
|
||||
spans.push(icon.to_span_with(|icon| format!("{icon} ")));
|
||||
}
|
||||
|
||||
spans.push(Span::raw(name.to_string()));
|
||||
|
||||
Cell::from(Spans::from(spans))
|
||||
}),
|
||||
ui::PickerColumn::new("flags", |item: &JumpMeta, _| {
|
||||
let mut flags = Vec::new();
|
||||
|
@ -3327,12 +3362,43 @@ fn changed_file_picker(cx: &mut Context) {
|
|||
|
||||
let columns = [
|
||||
PickerColumn::new("change", |change: &FileChange, data: &FileChangeData| {
|
||||
let icons = ICONS.load();
|
||||
match change {
|
||||
FileChange::Untracked { .. } => Span::styled("+ untracked", data.style_untracked),
|
||||
FileChange::Modified { .. } => Span::styled("~ modified", data.style_modified),
|
||||
FileChange::Conflict { .. } => Span::styled("x conflict", data.style_conflict),
|
||||
FileChange::Deleted { .. } => Span::styled("- deleted", data.style_deleted),
|
||||
FileChange::Renamed { .. } => Span::styled("> renamed", data.style_renamed),
|
||||
FileChange::Untracked { .. } => Span::styled(
|
||||
match icons.vcs().added() {
|
||||
Some(icon) => Cow::from(format!("{icon} untracked")),
|
||||
None => Cow::from("untracked"),
|
||||
},
|
||||
data.style_untracked,
|
||||
),
|
||||
FileChange::Modified { .. } => Span::styled(
|
||||
match icons.vcs().modified() {
|
||||
Some(icon) => Cow::from(format!("{icon} modified")),
|
||||
None => Cow::from("modified"),
|
||||
},
|
||||
data.style_modified,
|
||||
),
|
||||
FileChange::Conflict { .. } => Span::styled(
|
||||
match icons.vcs().conflict() {
|
||||
Some(icon) => Cow::from(format!("{icon} conflict")),
|
||||
None => Cow::from("conflict"),
|
||||
},
|
||||
data.style_conflict,
|
||||
),
|
||||
FileChange::Deleted { .. } => Span::styled(
|
||||
match icons.vcs().removed() {
|
||||
Some(icon) => Cow::from(format!("{icon} deleted")),
|
||||
None => Cow::from("deleted"),
|
||||
},
|
||||
data.style_deleted,
|
||||
),
|
||||
FileChange::Renamed { .. } => Span::styled(
|
||||
match icons.vcs().renamed() {
|
||||
Some(icon) => Cow::from(format!("{icon} renamed")),
|
||||
None => Cow::from("renamed"),
|
||||
},
|
||||
data.style_renamed,
|
||||
),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
|
|
|
@ -9,7 +9,10 @@ use helix_lsp::{
|
|||
Client, LanguageServerId, OffsetEncoding,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
use tui::{text::Span, widgets::Row};
|
||||
use tui::{
|
||||
text::{Span, ToSpan},
|
||||
widgets::Row,
|
||||
};
|
||||
|
||||
use super::{align_view, push_jump, Align, Context, Editor};
|
||||
|
||||
|
@ -22,6 +25,7 @@ use helix_view::{
|
|||
document::{DocumentInlayHints, DocumentInlayHintsId},
|
||||
editor::Action,
|
||||
handlers::lsp::SignatureHelpInvoked,
|
||||
icons::ICONS,
|
||||
theme::Style,
|
||||
Document, View,
|
||||
};
|
||||
|
@ -182,7 +186,7 @@ fn display_symbol_kind(kind: lsp::SymbolKind) -> &'static str {
|
|||
lsp::SymbolKind::OBJECT => "object",
|
||||
lsp::SymbolKind::KEY => "key",
|
||||
lsp::SymbolKind::NULL => "null",
|
||||
lsp::SymbolKind::ENUM_MEMBER => "enummem",
|
||||
lsp::SymbolKind::ENUM_MEMBER => "enum_member",
|
||||
lsp::SymbolKind::STRUCT => "struct",
|
||||
lsp::SymbolKind::EVENT => "event",
|
||||
lsp::SymbolKind::OPERATOR => "operator",
|
||||
|
@ -242,11 +246,23 @@ fn diag_picker(
|
|||
ui::PickerColumn::new(
|
||||
"severity",
|
||||
|item: &PickerDiagnostic, styles: &DiagnosticStyles| {
|
||||
let icons = ICONS.load();
|
||||
|
||||
match item.diag.severity {
|
||||
Some(DiagnosticSeverity::HINT) => Span::styled("HINT", styles.hint),
|
||||
Some(DiagnosticSeverity::INFORMATION) => Span::styled("INFO", styles.info),
|
||||
Some(DiagnosticSeverity::WARNING) => Span::styled("WARN", styles.warning),
|
||||
Some(DiagnosticSeverity::ERROR) => Span::styled("ERROR", styles.error),
|
||||
Some(DiagnosticSeverity::HINT) => {
|
||||
Span::styled(format!("{} HINT", icons.diagnostic().hint()), styles.hint)
|
||||
}
|
||||
Some(DiagnosticSeverity::INFORMATION) => {
|
||||
Span::styled(format!("{} INFO", icons.diagnostic().info()), styles.info)
|
||||
}
|
||||
Some(DiagnosticSeverity::WARNING) => Span::styled(
|
||||
format!("{} WARN", icons.diagnostic().warning()),
|
||||
styles.warning,
|
||||
),
|
||||
Some(DiagnosticSeverity::ERROR) => Span::styled(
|
||||
format!("{} ERROR", icons.diagnostic().error()),
|
||||
styles.error,
|
||||
),
|
||||
_ => Span::raw(""),
|
||||
}
|
||||
.into()
|
||||
|
@ -400,7 +416,15 @@ pub fn symbol_picker(cx: &mut Context) {
|
|||
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
|
||||
let columns = [
|
||||
ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
|
||||
display_symbol_kind(item.symbol.kind).into()
|
||||
let name = display_symbol_kind(item.symbol.kind);
|
||||
|
||||
let icons = ICONS.load();
|
||||
|
||||
if let Some(icon) = icons.kind().get(name) {
|
||||
icon.to_span_with(|icon| format!("{icon} {name}")).into()
|
||||
} else {
|
||||
name.into()
|
||||
}
|
||||
}),
|
||||
// Some symbols in the document symbol picker may have a URI that isn't
|
||||
// the current file. It should be rare though, so we concatenate that
|
||||
|
@ -518,7 +542,14 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
|||
};
|
||||
let columns = [
|
||||
ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
|
||||
display_symbol_kind(item.symbol.kind).into()
|
||||
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()
|
||||
} else {
|
||||
name.into()
|
||||
}
|
||||
}),
|
||||
ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
|
||||
item.symbol.name.as_str().into()
|
||||
|
|
|
@ -2,11 +2,13 @@ use crate::keymap;
|
|||
use crate::keymap::{merge_keys, KeyTrie};
|
||||
use helix_loader::merge_toml_values;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::icons::{Icons, ICONS};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::fs;
|
||||
use std::io::Error as IOError;
|
||||
use std::sync::Arc;
|
||||
use toml::de::Error as TomlError;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
@ -22,6 +24,7 @@ pub struct ConfigRaw {
|
|||
pub theme: Option<String>,
|
||||
pub keys: Option<HashMap<Mode, KeyTrie>>,
|
||||
pub editor: Option<toml::Value>,
|
||||
pub icons: Option<toml::Value>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
@ -64,6 +67,7 @@ impl Config {
|
|||
global.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig));
|
||||
let local_config: Result<ConfigRaw, ConfigLoadError> =
|
||||
local.and_then(|file| toml::from_str(&file).map_err(ConfigLoadError::BadConfig));
|
||||
|
||||
let res = match (global_config, local_config) {
|
||||
(Ok(global), Ok(local)) => {
|
||||
let mut keys = keymap::default();
|
||||
|
@ -84,6 +88,18 @@ impl Config {
|
|||
.map_err(ConfigLoadError::BadConfig)?,
|
||||
};
|
||||
|
||||
let icons: Icons = match (global.icons, local.icons) {
|
||||
(None, None) => Icons::default(),
|
||||
(None, Some(val)) | (Some(val), None) => {
|
||||
val.try_into().map_err(ConfigLoadError::BadConfig)?
|
||||
}
|
||||
(Some(global), Some(local)) => merge_toml_values(global, local, 3)
|
||||
.try_into()
|
||||
.map_err(ConfigLoadError::BadConfig)?,
|
||||
};
|
||||
|
||||
ICONS.store(Arc::new(icons));
|
||||
|
||||
Config {
|
||||
theme: local.theme.or(global.theme),
|
||||
keys,
|
||||
|
@ -100,6 +116,14 @@ impl Config {
|
|||
if let Some(keymap) = config.keys {
|
||||
merge_keys(&mut keys, keymap);
|
||||
}
|
||||
|
||||
let icons = config.icons.map_or_else(
|
||||
|| Ok(Icons::default()),
|
||||
|val| val.try_into().map_err(ConfigLoadError::BadConfig),
|
||||
)?;
|
||||
|
||||
ICONS.store(Arc::new(icons));
|
||||
|
||||
Config {
|
||||
theme: config.theme,
|
||||
keys,
|
||||
|
|
|
@ -8,6 +8,7 @@ use helix_view::{
|
|||
document::DocumentColorSwatches,
|
||||
events::{DocumentDidChange, DocumentDidOpen, LanguageServerExited, LanguageServerInitialized},
|
||||
handlers::{lsp::DocumentColorsEvent, Handlers},
|
||||
icons::ICONS,
|
||||
DocumentId, Editor, Theme,
|
||||
};
|
||||
use tokio::time::Instant;
|
||||
|
@ -124,9 +125,11 @@ fn attach_document_colors(
|
|||
let mut color_swatches_padding = Vec::with_capacity(doc_colors.len());
|
||||
let mut colors = Vec::with_capacity(doc_colors.len());
|
||||
|
||||
let icons = ICONS.load();
|
||||
|
||||
for (pos, color) in doc_colors {
|
||||
color_swatches_padding.push(InlineAnnotation::new(pos, " "));
|
||||
color_swatches.push(InlineAnnotation::new(pos, "■"));
|
||||
color_swatches.push(InlineAnnotation::new(pos, icons.kind().color().glyph()));
|
||||
colors.push(Theme::rgb_highlight(
|
||||
(color.red * 255.) as u8,
|
||||
(color.green * 255.) as u8,
|
||||
|
|
|
@ -9,6 +9,7 @@ use crate::{
|
|||
use helix_core::snippets::{ActiveSnippet, RenderedSnippet, Snippet};
|
||||
use helix_core::{self as core, chars, fuzzy::MATCHER, Change, Transaction};
|
||||
use helix_lsp::{lsp, util, OffsetEncoding};
|
||||
use helix_view::icons::ICONS;
|
||||
use helix_view::{
|
||||
editor::CompleteAction,
|
||||
handlers::lsp::SignatureHelpInvoked,
|
||||
|
@ -45,7 +46,7 @@ impl menu::Item for CompletionItem {
|
|||
CompletionItem::Other(core::CompletionItem { label, .. }) => label,
|
||||
};
|
||||
|
||||
let kind = match self {
|
||||
let mut kind = match self {
|
||||
CompletionItem::Lsp(LspCompletionItem { item, .. }) => match item.kind {
|
||||
Some(lsp::CompletionItemKind::TEXT) => "text".into(),
|
||||
Some(lsp::CompletionItemKind::METHOD) => "method".into(),
|
||||
|
@ -78,9 +79,13 @@ impl menu::Item for CompletionItem {
|
|||
})
|
||||
.and_then(Color::from_hex)
|
||||
.map_or("color".into(), |color| {
|
||||
let icons = ICONS.load();
|
||||
Spans::from(vec![
|
||||
Span::raw("color "),
|
||||
Span::styled("■", Style::default().fg(color)),
|
||||
Span::styled(
|
||||
icons.kind().color().glyph().to_string(),
|
||||
Style::default().fg(color),
|
||||
),
|
||||
])
|
||||
}),
|
||||
Some(lsp::CompletionItemKind::FILE) => "file".into(),
|
||||
|
@ -101,11 +106,28 @@ impl menu::Item for CompletionItem {
|
|||
CompletionItem::Other(core::CompletionItem { kind, .. }) => kind.as_ref().into(),
|
||||
};
|
||||
|
||||
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) {
|
||||
kind.0[0].content = format!("{icon} {name}").into();
|
||||
|
||||
if let Some(style) = icon.color().map(|color| Style::default().fg(color)) {
|
||||
kind.0[0].style = style;
|
||||
} else if is_folder {
|
||||
kind.0[0].style = *dir_style;
|
||||
}
|
||||
} else {
|
||||
kind.0[0].content = format!("{name}").into();
|
||||
}
|
||||
|
||||
let label = Span::styled(
|
||||
label,
|
||||
if deprecated {
|
||||
Style::default().add_modifier(Modifier::CROSSED_OUT)
|
||||
} else if kind.0[0].content == "folder" {
|
||||
} else if is_folder {
|
||||
*dir_style
|
||||
} else {
|
||||
Style::default()
|
||||
|
|
|
@ -9,6 +9,7 @@ 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};
|
||||
|
@ -206,38 +207,41 @@ impl<'a> TextRenderer<'a> {
|
|||
viewport: Rect,
|
||||
) -> TextRenderer<'a> {
|
||||
let editor_config = doc.config.load();
|
||||
let WhitespaceConfig {
|
||||
render: ws_render,
|
||||
characters: ws_chars,
|
||||
} = &editor_config.whitespace;
|
||||
|
||||
let WhitespaceConfig { render } = &editor_config.whitespace;
|
||||
|
||||
let tab_width = doc.tab_width();
|
||||
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))
|
||||
|
||||
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))
|
||||
.collect()
|
||||
} else {
|
||||
" ".repeat(tab_width)
|
||||
};
|
||||
let virtual_tab = " ".repeat(tab_width);
|
||||
let newline = if ws_render.newline() == WhitespaceRenderValue::All {
|
||||
ws_chars.newline.into()
|
||||
let newline = if render.newline() == WhitespaceRenderValue::All {
|
||||
whitespace.newline().into()
|
||||
} else {
|
||||
" ".to_owned()
|
||||
};
|
||||
|
||||
let space = if ws_render.space() == WhitespaceRenderValue::All {
|
||||
ws_chars.space.into()
|
||||
let space = if render.space() == WhitespaceRenderValue::All {
|
||||
whitespace.space().into()
|
||||
} else {
|
||||
" ".to_owned()
|
||||
};
|
||||
let nbsp = if ws_render.nbsp() == WhitespaceRenderValue::All {
|
||||
ws_chars.nbsp.into()
|
||||
let nbsp = if render.nbsp() == WhitespaceRenderValue::All {
|
||||
whitespace.nbsp().into()
|
||||
} else {
|
||||
" ".to_owned()
|
||||
};
|
||||
let nnbsp = if ws_render.nnbsp() == WhitespaceRenderValue::All {
|
||||
ws_chars.nnbsp.into()
|
||||
let nnbsp = if render.nnbsp() == WhitespaceRenderValue::All {
|
||||
whitespace.nnbsp().into()
|
||||
} else {
|
||||
" ".to_owned()
|
||||
};
|
||||
|
@ -271,6 +275,7 @@ 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,
|
||||
|
|
|
@ -24,9 +24,10 @@ use helix_core::{
|
|||
};
|
||||
use helix_view::{
|
||||
annotations::diagnostics::DiagnosticFilter,
|
||||
document::{Mode, SCRATCH_BUFFER_NAME},
|
||||
document::{Mode, DEFAULT_LANGUAGE_NAME, SCRATCH_BUFFER_NAME},
|
||||
editor::{CompleteAction, CursorShapeConfig},
|
||||
graphics::{Color, CursorKind, Modifier, Rect, Style},
|
||||
icons::ICONS,
|
||||
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
|
||||
keyboard::{KeyCode, KeyModifiers},
|
||||
Document, Editor, Theme, View,
|
||||
|
@ -234,7 +235,8 @@ impl EditorView {
|
|||
theme: &Theme,
|
||||
) {
|
||||
let editor_rulers = &editor.config().rulers;
|
||||
let ruler_theme = theme
|
||||
|
||||
let style = theme
|
||||
.try_get("ui.virtual.ruler")
|
||||
.unwrap_or_else(|| Style::default().bg(Color::Red));
|
||||
|
||||
|
@ -252,7 +254,12 @@ 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| surface.set_style(area, ruler_theme))
|
||||
.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn viewport_byte_range(
|
||||
|
@ -597,7 +604,30 @@ impl EditorView {
|
|||
bufferline_inactive
|
||||
};
|
||||
|
||||
let text = format!(" {}{} ", fname, if doc.is_modified() { "[+]" } else { "" });
|
||||
let icons = ICONS.load();
|
||||
|
||||
let lang = doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME);
|
||||
|
||||
if let Some(icon) = icons
|
||||
.fs()
|
||||
.from_optional_path_or_lang(doc.path().map(|path| path.as_path()), lang)
|
||||
{
|
||||
let used_width = viewport.x.saturating_sub(x);
|
||||
let rem_width = surface.area.width.saturating_sub(used_width);
|
||||
|
||||
let style = icon.color().map_or(style, |color| style.fg(color));
|
||||
|
||||
x = surface
|
||||
.set_stringn(x, viewport.y, format!(" {icon}"), rem_width as usize, style)
|
||||
.0;
|
||||
|
||||
if x >= surface.area.right() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let text = format!(" {} {}", fname, if doc.is_modified() { "[+] " } else { "" });
|
||||
|
||||
let used_width = viewport.x.saturating_sub(x);
|
||||
let rem_width = surface.area.width.saturating_sub(used_width);
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ use crate::job::{self, Callback};
|
|||
pub use completion::Completion;
|
||||
pub use editor::EditorView;
|
||||
use helix_stdx::rope;
|
||||
use helix_view::icons::ICONS;
|
||||
use helix_view::theme::Style;
|
||||
pub use markdown::Markdown;
|
||||
pub use menu::Menu;
|
||||
|
@ -30,7 +31,8 @@ pub use spinner::{ProgressSpinners, Spinner};
|
|||
pub use text::Text;
|
||||
|
||||
use helix_view::Editor;
|
||||
use tui::text::{Span, Spans};
|
||||
use tui::text::{Span, Spans, ToSpan};
|
||||
use tui::widgets::Cell;
|
||||
|
||||
use std::path::Path;
|
||||
use std::{error::Error, path::PathBuf};
|
||||
|
@ -249,7 +251,14 @@ pub fn file_picker(editor: &Editor, root: PathBuf) -> FilePicker {
|
|||
"path",
|
||||
|item: &PathBuf, data: &FilePickerData| {
|
||||
let path = item.strip_prefix(&data.root).unwrap_or(item);
|
||||
let mut spans = Vec::with_capacity(3);
|
||||
let mut spans = Vec::with_capacity(4);
|
||||
|
||||
let icons = ICONS.load();
|
||||
|
||||
if let Some(icon) = icons.fs().from_path(path) {
|
||||
spans.push(icon.to_span_with(|icon| format!("{icon} ")));
|
||||
}
|
||||
|
||||
if let Some(dirs) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
|
||||
spans.extend([
|
||||
Span::styled(dirs.to_string_lossy(), data.directory_style),
|
||||
|
@ -310,8 +319,22 @@ pub fn file_explorer(root: PathBuf, editor: &Editor) -> Result<FileExplorer, std
|
|||
"path",
|
||||
|(path, is_dir): &(PathBuf, bool), (root, directory_style): &(PathBuf, Style)| {
|
||||
let name = path.strip_prefix(root).unwrap_or(path).to_string_lossy();
|
||||
|
||||
let icons = ICONS.load();
|
||||
|
||||
if *is_dir {
|
||||
Span::styled(format!("{}/", name), *directory_style).into()
|
||||
if let Some(icon) = icons.fs().directory(false) {
|
||||
Span::styled(format!("{icon} {name}/"), *directory_style).into()
|
||||
} else {
|
||||
Span::styled(format!("{name}/"), *directory_style).into()
|
||||
}
|
||||
} else if let Some(icon) = icons.fs().from_path(path) {
|
||||
let mut spans = Vec::with_capacity(2);
|
||||
|
||||
spans.push(icon.to_span_with(|icon| format!("{icon} ")));
|
||||
spans.push(Span::raw(name));
|
||||
|
||||
Cell::from(Spans::from(spans))
|
||||
} else {
|
||||
name.into()
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use helix_core::indent::IndentStyle;
|
|||
use helix_core::{coords_at_pos, encoding, Position};
|
||||
use helix_lsp::lsp::DiagnosticSeverity;
|
||||
use helix_view::document::DEFAULT_LANGUAGE_NAME;
|
||||
use helix_view::icons::ICONS;
|
||||
use helix_view::{
|
||||
document::{Mode, SCRATCH_BUFFER_NAME},
|
||||
graphics::Rect,
|
||||
|
@ -15,7 +16,7 @@ use crate::ui::ProgressSpinners;
|
|||
|
||||
use helix_view::editor::StatusLineElement as StatusLineElementID;
|
||||
use tui::buffer::Buffer as Surface;
|
||||
use tui::text::{Span, Spans};
|
||||
use tui::text::{Span, Spans, ToSpan};
|
||||
|
||||
pub struct RenderContext<'a> {
|
||||
pub editor: &'a Editor,
|
||||
|
@ -234,29 +235,49 @@ where
|
|||
counts
|
||||
});
|
||||
|
||||
let icons = ICONS.load();
|
||||
|
||||
for sev in &context.editor.config().statusline.diagnostics {
|
||||
match sev {
|
||||
Severity::Hint if hints > 0 => {
|
||||
write(context, Span::styled("●", context.editor.theme.get("hint")));
|
||||
write(context, format!(" {} ", hints).into());
|
||||
write(
|
||||
context,
|
||||
Span::styled(
|
||||
icons.diagnostic().hint().to_string(),
|
||||
context.editor.theme.get("hint"),
|
||||
),
|
||||
);
|
||||
write(context, Span::raw(format!(" {hints} ")));
|
||||
}
|
||||
Severity::Info if info > 0 => {
|
||||
write(context, Span::styled("●", context.editor.theme.get("info")));
|
||||
write(context, format!(" {} ", info).into());
|
||||
write(
|
||||
context,
|
||||
Span::styled(
|
||||
icons.diagnostic().info().to_string(),
|
||||
context.editor.theme.get("info"),
|
||||
),
|
||||
);
|
||||
write(context, Span::raw(format!(" {info} ")));
|
||||
}
|
||||
Severity::Warning if warnings > 0 => {
|
||||
write(
|
||||
context,
|
||||
Span::styled("●", context.editor.theme.get("warning")),
|
||||
Span::styled(
|
||||
icons.diagnostic().warning().to_string(),
|
||||
context.editor.theme.get("warning"),
|
||||
),
|
||||
);
|
||||
write(context, format!(" {} ", warnings).into());
|
||||
write(context, Span::raw(format!(" {warnings} ")));
|
||||
}
|
||||
Severity::Error if errors > 0 => {
|
||||
write(
|
||||
context,
|
||||
Span::styled("●", context.editor.theme.get("error")),
|
||||
Span::styled(
|
||||
icons.diagnostic().error().to_string(),
|
||||
context.editor.theme.get("error"),
|
||||
),
|
||||
);
|
||||
write(context, format!(" {} ", errors).into());
|
||||
write(context, Span::raw(format!(" {errors} ")));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@ -287,10 +308,10 @@ where
|
|||
},
|
||||
);
|
||||
|
||||
let sevs_to_show = &context.editor.config().statusline.workspace_diagnostics;
|
||||
let sevs = &context.editor.config().statusline.workspace_diagnostics;
|
||||
|
||||
// Avoid showing the " W " if no diagnostic counts will be shown.
|
||||
if !sevs_to_show.iter().any(|sev| match sev {
|
||||
// Avoid showing the ` W ` if no diagnostic counts will be shown.
|
||||
if !sevs.iter().any(|sev| match sev {
|
||||
Severity::Hint => hints != 0,
|
||||
Severity::Info => info != 0,
|
||||
Severity::Warning => warnings != 0,
|
||||
|
@ -299,31 +320,57 @@ where
|
|||
return;
|
||||
}
|
||||
|
||||
write(context, " W ".into());
|
||||
let icons = ICONS.load();
|
||||
|
||||
for sev in sevs_to_show {
|
||||
let icon = icons.ui().workspace();
|
||||
|
||||
// Special case when the `workspace` key is set to `""`:
|
||||
// - This will remove the default ` W ` so that the rest of the icons are spaced correctly.
|
||||
if !icon.glyph().is_empty() {
|
||||
write(context, icon.to_span_with(|icon| format!(" {icon} ")));
|
||||
}
|
||||
|
||||
for sev in sevs {
|
||||
match sev {
|
||||
Severity::Hint if hints > 0 => {
|
||||
write(context, Span::styled("●", context.editor.theme.get("hint")));
|
||||
write(context, format!(" {} ", hints).into());
|
||||
write(
|
||||
context,
|
||||
Span::styled(
|
||||
icons.diagnostic().hint().to_string(),
|
||||
context.editor.theme.get("hint"),
|
||||
),
|
||||
);
|
||||
write(context, Span::raw(format!(" {hints} ")));
|
||||
}
|
||||
Severity::Info if info > 0 => {
|
||||
write(context, Span::styled("●", context.editor.theme.get("info")));
|
||||
write(context, format!(" {} ", info).into());
|
||||
write(
|
||||
context,
|
||||
Span::styled(
|
||||
format!(" {} ", icons.diagnostic().info()),
|
||||
context.editor.theme.get("info"),
|
||||
),
|
||||
);
|
||||
write(context, Span::raw(format!(" {info} ")));
|
||||
}
|
||||
Severity::Warning if warnings > 0 => {
|
||||
write(
|
||||
context,
|
||||
Span::styled("●", context.editor.theme.get("warning")),
|
||||
Span::styled(
|
||||
icons.diagnostic().warning().to_string(),
|
||||
context.editor.theme.get("warning"),
|
||||
),
|
||||
);
|
||||
write(context, format!(" {} ", warnings).into());
|
||||
write(context, Span::raw(format!(" {warnings} ")));
|
||||
}
|
||||
Severity::Error if errors > 0 => {
|
||||
write(
|
||||
context,
|
||||
Span::styled("●", context.editor.theme.get("error")),
|
||||
Span::styled(
|
||||
icons.diagnostic().error().to_string(),
|
||||
context.editor.theme.get("error"),
|
||||
),
|
||||
);
|
||||
write(context, format!(" {} ", errors).into());
|
||||
write(context, Span::raw(format!(" {errors} ")));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
@ -440,9 +487,18 @@ fn render_file_type<'a, F>(context: &mut RenderContext<'a>, write: F)
|
|||
where
|
||||
F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy,
|
||||
{
|
||||
let file_type = context.doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME);
|
||||
let icons = ICONS.load();
|
||||
|
||||
write(context, format!(" {} ", file_type).into());
|
||||
let lang = context.doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME);
|
||||
|
||||
if let Some(icon) = icons
|
||||
.fs()
|
||||
.from_optional_path_or_lang(context.doc.path().map(|path| path.as_path()), lang)
|
||||
{
|
||||
write(context, icon.to_span_with(|icon| format!(" {icon} ")));
|
||||
} else {
|
||||
write(context, format!(" {lang} ").into());
|
||||
}
|
||||
}
|
||||
|
||||
fn render_file_name<'a, F>(context: &mut RenderContext<'a>, write: F)
|
||||
|
@ -522,10 +578,13 @@ 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");
|
||||
|
||||
write(context, Span::styled(sep.to_string(), style));
|
||||
let icons = ICONS.load();
|
||||
|
||||
let separator = icons.ui().statusline().separator().to_string();
|
||||
|
||||
write(context, Span::styled(separator, style));
|
||||
}
|
||||
|
||||
fn render_spacer<'a, F>(context: &mut RenderContext<'a>, write: F)
|
||||
|
@ -539,13 +598,18 @@ fn render_version_control<'a, F>(context: &mut RenderContext<'a>, write: F)
|
|||
where
|
||||
F: Fn(&mut RenderContext<'a>, Span<'a>) + Copy,
|
||||
{
|
||||
let head = context
|
||||
.doc
|
||||
.version_control_head()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let head = context.doc.version_control_head().unwrap_or_default();
|
||||
|
||||
write(context, head.into());
|
||||
if !head.is_empty() {
|
||||
let icons = ICONS.load();
|
||||
|
||||
let vcs = match icons.vcs().branch() {
|
||||
Some(icon) => format!(" {icon} {head} "),
|
||||
None => format!(" {head} "),
|
||||
};
|
||||
|
||||
write(context, vcs.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn render_register<'a, F>(context: &mut RenderContext<'a>, write: F)
|
||||
|
|
|
@ -9,6 +9,7 @@ use helix_view::annotations::diagnostics::{
|
|||
DiagnosticFilter, InlineDiagnosticAccumulator, InlineDiagnosticsConfig,
|
||||
};
|
||||
|
||||
use helix_view::icons::ICONS;
|
||||
use helix_view::theme::Style;
|
||||
use helix_view::{Document, Theme};
|
||||
|
||||
|
@ -102,6 +103,24 @@ impl Renderer<'_, '_> {
|
|||
let mut end_col = start_col;
|
||||
let mut draw_col = (col + 1) as u16;
|
||||
|
||||
// Draw the diagnostic indicator:
|
||||
if !self.renderer.column_in_bounds(draw_col as usize, 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let icons = ICONS.load();
|
||||
|
||||
let symbol = match diag.severity() {
|
||||
Severity::Hint => icons.diagnostic().hint(),
|
||||
Severity::Info => icons.diagnostic().info(),
|
||||
Severity::Warning => icons.diagnostic().warning(),
|
||||
Severity::Error => icons.diagnostic().error(),
|
||||
};
|
||||
|
||||
self.renderer
|
||||
.set_string(self.renderer.viewport.x + draw_col, row, symbol, style);
|
||||
draw_col += 2;
|
||||
|
||||
for line in diag.message.lines() {
|
||||
if !self.renderer.column_in_bounds(draw_col as usize, 1) {
|
||||
break;
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
use helix_core::line_ending::str_is_line_ending;
|
||||
use helix_core::unicode::width::UnicodeWidthStr;
|
||||
use helix_view::graphics::Style;
|
||||
use helix_view::icons::Icon;
|
||||
use std::borrow::Cow;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
|
@ -208,6 +209,20 @@ impl<'a> From<Cow<'a, str>> for Span<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait ToSpan<'a> {
|
||||
fn to_span_with(&self, content: impl Fn(&Self) -> String) -> Span<'a>;
|
||||
}
|
||||
|
||||
impl<'a> ToSpan<'a> for Icon {
|
||||
fn to_span_with(&self, content: impl Fn(&Self) -> String) -> Span<'a> {
|
||||
if let Some(style) = self.color().map(|color| Style::default().fg(color)) {
|
||||
Span::styled(content(self), style)
|
||||
} else {
|
||||
Span::raw(content(self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A string composed of clusters of graphemes, each with their own style.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct Spans<'a>(pub Vec<Span<'a>>);
|
||||
|
|
|
@ -53,6 +53,7 @@ parking_lot.workspace = true
|
|||
thiserror.workspace = true
|
||||
|
||||
kstring = "2.0"
|
||||
smartstring = { version = "1.0.1", features = ["serde"]}
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
clipboard-win = { version = "5.4", features = ["std"] }
|
||||
|
|
|
@ -43,6 +43,7 @@ use helix_core::{
|
|||
ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction,
|
||||
};
|
||||
|
||||
use crate::icons::{Icons, ICONS};
|
||||
use crate::{
|
||||
editor::Config,
|
||||
events::{DocumentDidChange, SelectionDidChange},
|
||||
|
@ -2243,8 +2244,10 @@ impl Document {
|
|||
.unwrap_or(40);
|
||||
let wrap_indicator = language_soft_wrap
|
||||
.and_then(|soft_wrap| soft_wrap.wrap_indicator.clone())
|
||||
.or_else(|| config.soft_wrap.wrap_indicator.clone())
|
||||
.unwrap_or_else(|| "↪ ".into());
|
||||
.unwrap_or_else(|| {
|
||||
let icons: arc_swap::access::DynGuard<Icons> = ICONS.load();
|
||||
icons.ui().r#virtual().wrap().to_string()
|
||||
});
|
||||
let tab_width = self.tab_width() as u16;
|
||||
TextFormat {
|
||||
soft_wrap: enable_soft_wrap && viewport_width > 10,
|
||||
|
|
|
@ -508,7 +508,6 @@ 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>,
|
||||
|
@ -534,7 +533,6 @@ 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],
|
||||
|
@ -753,14 +751,12 @@ 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -886,30 +882,6 @@ 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 {
|
||||
|
|
|
@ -5,6 +5,7 @@ use helix_core::syntax::config::LanguageServerFeature;
|
|||
use crate::{
|
||||
editor::GutterType,
|
||||
graphics::{Style, UnderlineStyle},
|
||||
icons::ICONS,
|
||||
Document, Editor, Theme, View,
|
||||
};
|
||||
|
||||
|
@ -46,7 +47,6 @@ impl GutterType {
|
|||
}
|
||||
|
||||
pub fn diagnostic<'doc>(
|
||||
_editor: &'doc Editor,
|
||||
doc: &'doc Document,
|
||||
_view: &View,
|
||||
theme: &Theme,
|
||||
|
@ -74,15 +74,20 @@ pub fn diagnostic<'doc>(
|
|||
.any(|ls| ls.id() == id)
|
||||
})
|
||||
});
|
||||
diagnostics_on_line.max_by_key(|d| d.severity).map(|d| {
|
||||
write!(out, "●").ok();
|
||||
match d.severity {
|
||||
Some(Severity::Error) => error,
|
||||
Some(Severity::Warning) | None => warning,
|
||||
Some(Severity::Info) => info,
|
||||
Some(Severity::Hint) => hint,
|
||||
}
|
||||
})
|
||||
|
||||
diagnostics_on_line
|
||||
.max_by_key(|d| d.severity)
|
||||
.map(move |d| {
|
||||
let icons = ICONS.load();
|
||||
let (style, symbol) = match d.severity {
|
||||
Some(Severity::Error) => (error, icons.diagnostic().error()),
|
||||
Some(Severity::Warning) | None => (warning, icons.diagnostic().warning()),
|
||||
Some(Severity::Info) => (info, icons.diagnostic().info()),
|
||||
Some(Severity::Hint) => (hint, icons.diagnostic().hint()),
|
||||
};
|
||||
out.push_str(symbol);
|
||||
style
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -119,18 +124,21 @@ pub fn diff<'doc>(
|
|||
return None;
|
||||
}
|
||||
|
||||
let icons = ICONS.load();
|
||||
|
||||
let (icon, style) = if hunk.is_pure_insertion() {
|
||||
("▍", added)
|
||||
(icons.ui().gutter().added(), added)
|
||||
} else if hunk.is_pure_removal() {
|
||||
if !first_visual_line {
|
||||
return None;
|
||||
}
|
||||
("▔", deleted)
|
||||
(icons.ui().gutter().removed(), deleted)
|
||||
} else {
|
||||
("▍", modified)
|
||||
(icons.ui().gutter().modified(), modified)
|
||||
};
|
||||
|
||||
write!(out, "{}", icon).unwrap();
|
||||
write!(out, "{icon}").unwrap();
|
||||
|
||||
Some(style)
|
||||
},
|
||||
)
|
||||
|
@ -264,7 +272,13 @@ pub fn breakpoints<'doc>(
|
|||
breakpoint_style
|
||||
};
|
||||
|
||||
let sym = if breakpoint.verified { "●" } else { "◯" };
|
||||
let icons = ICONS.load();
|
||||
|
||||
let sym = if breakpoint.verified {
|
||||
icons.dap().verified()
|
||||
} else {
|
||||
icons.dap().unverified()
|
||||
};
|
||||
write!(out, "{}", sym).unwrap();
|
||||
Some(style)
|
||||
},
|
||||
|
@ -299,8 +313,9 @@ fn execution_pause_indicator<'doc>(
|
|||
return None;
|
||||
}
|
||||
|
||||
let sym = "▶";
|
||||
write!(out, "{}", sym).unwrap();
|
||||
let icons = ICONS.load();
|
||||
|
||||
write!(out, "{}", icons.dap().play()).unwrap();
|
||||
Some(style)
|
||||
},
|
||||
)
|
||||
|
@ -313,7 +328,7 @@ pub fn diagnostics_or_breakpoints<'doc>(
|
|||
theme: &Theme,
|
||||
is_focused: bool,
|
||||
) -> GutterFn<'doc> {
|
||||
let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused);
|
||||
let mut diagnostics = diagnostic(doc, view, theme, is_focused);
|
||||
let mut breakpoints = breakpoints(editor, doc, view, theme, is_focused);
|
||||
let mut execution_pause_indicator = execution_pause_indicator(editor, doc, theme, is_focused);
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -11,6 +11,7 @@ pub mod expansion;
|
|||
pub mod graphics;
|
||||
pub mod gutter;
|
||||
pub mod handlers;
|
||||
pub mod icons;
|
||||
pub mod info;
|
||||
pub mod input;
|
||||
pub mod keyboard;
|
||||
|
|
Loading…
Reference in New Issue