Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Color swatches ( 🟩 green 🟥 #ffaaaa ) #12308

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7186aa4
feat: basic implementation
nik-rev Dec 19, 2024
c71d63a
feat: use different color
nik-rev Dec 19, 2024
9b25200
feat: remove unneeded comment
nik-rev Dec 19, 2024
9f15eb6
hack the text_decorations to make this work
gabydd Dec 20, 2024
f135f67
propogate the colours
gabydd Dec 20, 2024
99ce9d2
refactor: compute_lines function
nik-rev Dec 20, 2024
6eea670
feat: move inlay_hints computations earlier
nik-rev Dec 20, 2024
d02add6
refactor: move inlay hints computation function earlier
nik-rev Dec 20, 2024
9890fac
refactor: extract ColorSwatch into a separate struct
nik-rev Dec 20, 2024
609a273
style: final changes, moving around structs
nik-rev Dec 20, 2024
419de43
refactor: remove unused imports
nik-rev Dec 20, 2024
ef0e702
feat: add space
nik-rev Dec 20, 2024
2f4cda6
perf: use `with_capacity` since we know size of the vec
nik-rev Dec 20, 2024
8e5b92e
docs: mention `display-color-swatches`
nik-rev Dec 20, 2024
e7dab91
refactor: merge into one statement
nik-rev Dec 20, 2024
0cf8b8c
refactor: check for is_none
nik-rev Dec 23, 2024
3bf6c9f
refactor: use let else statement
nik-rev Dec 23, 2024
3ccdb61
refactor: rename variable
nik-rev Dec 23, 2024
bbb7c11
refactor: use let else statement
nik-rev Dec 23, 2024
ab06d53
refactor: use let else statement
nik-rev Dec 23, 2024
09c3177
feat: use padding for color swatches
nik-rev Dec 23, 2024
4cecd0c
chore: apply suggestion from clippy lint
nik-rev Dec 23, 2024
8a3590e
perf: use a single iteration over the views of a document
nik-rev Dec 25, 2024
d0c6b04
refactor: rename several variables
nik-rev Dec 25, 2024
a60d41f
refactor: rename function
nik-rev Dec 25, 2024
277343f
refactor: inline_annotations_range moved to be on Document
nik-rev Dec 25, 2024
3ae169f
docs: improve description of what the calculate range inline method does
nik-rev Dec 25, 2024
5f8b5e9
fix: do not highlight wrap indicator
nik-rev Jan 27, 2025
5a0a773
fix: use Highlight enum for code blocks
nik-rev Jan 27, 2025
f21f974
docs: improve wording
nik-rev Jan 27, 2025
3153d5c
refactor: rename some variables
nik-rev Jan 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions book/src/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ The following statusline elements can be configured:
| `display-progress-messages` | Display LSP progress messages below statusline[^1] | `false` |
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
| `display-inlay-hints` | Display inlay hints[^2] | `false` |
| `display-color-swatches` | Shows color swatches next to colors | `true` |
| `display-signature-help-docs` | Display docs under signature help popup | `true` |
| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` |
| `goto-reference-include-declaration` | Include declaration in the goto references popup. | `true` |
Expand Down
27 changes: 17 additions & 10 deletions helix-core/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ pub enum LanguageServerFeature {
Diagnostics,
RenameSymbol,
InlayHints,
ColorProvider,
}

impl Display for LanguageServerFeature {
Expand All @@ -357,6 +358,7 @@ impl Display for LanguageServerFeature {
Diagnostics => "diagnostics",
RenameSymbol => "rename-symbol",
InlayHints => "inlay-hints",
ColorProvider => "color-provider",
};
write!(f, "{feature}",)
}
Expand Down Expand Up @@ -1770,7 +1772,12 @@ const CANCELLATION_CHECK_INTERVAL: usize = 100;

/// Indicates which highlight should be applied to a region of source code.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Highlight(pub usize);
pub enum Highlight {
/// When we use this type of highlight, we index into the Theme's scopes to get the Style
Indexed(usize),
/// A custom color, not dependent on the theme. Represents (red, green, blue)
Rgb(u8, u8, u8),
}

/// Represents the reason why syntax highlighting failed.
#[derive(Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -2029,7 +2036,7 @@ impl HighlightConfiguration {
best_match_len = len;
}
}
best_index.map(Highlight)
best_index.map(Highlight::Indexed)
})
.collect();

Expand Down Expand Up @@ -2559,18 +2566,18 @@ const SHEBANG: &str = r"#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)";

pub struct Merge<I> {
iter: I,
spans: Box<dyn Iterator<Item = (usize, std::ops::Range<usize>)>>,
spans: Box<dyn Iterator<Item = (Highlight, std::ops::Range<usize>)>>,

next_event: Option<HighlightEvent>,
next_span: Option<(usize, std::ops::Range<usize>)>,
next_span: Option<(Highlight, std::ops::Range<usize>)>,

queue: Vec<HighlightEvent>,
}

/// Merge a list of spans into the highlight event stream.
pub fn merge<I: Iterator<Item = HighlightEvent>>(
iter: I,
spans: Vec<(usize, std::ops::Range<usize>)>,
spans: Vec<(Highlight, std::ops::Range<usize>)>,
) -> Merge<I> {
let spans = Box::new(spans.into_iter());
let mut merge = Merge {
Expand Down Expand Up @@ -2636,9 +2643,9 @@ impl<I: Iterator<Item = HighlightEvent>> Iterator for Merge<I> {

Some(event)
}
(Some(Source { start, end }), Some((span, range))) if start == range.start => {
(Some(Source { start, end }), Some((highlight, range))) if start == range.start => {
let intersect = range.end.min(end);
let event = HighlightStart(Highlight(*span));
let event = HighlightStart(*highlight);

// enqueue in reverse order
self.queue.push(HighlightEnd);
Expand All @@ -2661,7 +2668,7 @@ impl<I: Iterator<Item = HighlightEvent>> Iterator for Merge<I> {
if intersect == range.end {
self.next_span = self.spans.next();
} else {
self.next_span = Some((*span, intersect..range.end));
self.next_span = Some((*highlight, intersect..range.end));
}

Some(event)
Expand All @@ -2675,8 +2682,8 @@ impl<I: Iterator<Item = HighlightEvent>> Iterator for Merge<I> {
// even though the range is past the end of the text. This needs to be
// handled appropriately by the drawing code by not assuming that
// all `Source` events point to valid indices in the rope.
(None, Some((span, range))) => {
let event = HighlightStart(Highlight(*span));
(None, Some((highlight, range))) => {
let event = HighlightStart(*highlight);
self.queue.push(HighlightEnd);
self.queue.push(Source {
start: range.start,
Expand Down
4 changes: 2 additions & 2 deletions helix-core/src/text_annotations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,15 +303,15 @@ impl<'a> TextAnnotations<'a> {
pub fn collect_overlay_highlights(
&self,
char_range: Range<usize>,
) -> Vec<(usize, Range<usize>)> {
) -> Vec<(Highlight, Range<usize>)> {
let mut highlights = Vec::new();
self.reset_pos(char_range.start);
for char_idx in char_range {
if let Some((_, Some(highlight))) = self.overlay_at(char_idx) {
// we don't know the number of chars the original grapheme takes
// however it doesn't matter as highlight boundaries are automatically
// aligned to grapheme boundaries in the rendering code
highlights.push((highlight.0, char_idx..char_idx + 1))
highlights.push((highlight, char_idx..char_idx + 1))
}
}

Expand Down
20 changes: 20 additions & 0 deletions helix-lsp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ impl Client {
capabilities.inlay_hint_provider,
Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_)))
),
LanguageServerFeature::ColorProvider => capabilities.color_provider.is_some(),
}
}

Expand Down Expand Up @@ -1114,6 +1115,25 @@ impl Client {
Some(self.call::<lsp::request::InlayHintRequest>(params))
}

pub fn text_document_color_swatches(
&self,
text_document: lsp::TextDocumentIdentifier,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
self.capabilities.get().unwrap().color_provider.as_ref()?;
let params = lsp::DocumentColorParams {
text_document,
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: work_done_token.clone(),
},
partial_result_params: helix_lsp_types::PartialResultParams {
partial_result_token: work_done_token,
},
};

Some(self.call::<lsp::request::DocumentColor>(params))
}

pub fn text_document_hover(
&self,
text_document: lsp::TextDocumentIdentifier,
Expand Down
149 changes: 125 additions & 24 deletions helix-term/src/commands/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ use tui::{text::Span, widgets::Row};
use super::{align_view, push_jump, Align, Context, Editor};

use helix_core::{
syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection, Uri,
syntax::{Highlight, LanguageServerFeature},
text_annotations::InlineAnnotation,
Selection, Uri,
};
use helix_stdx::path;
use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId},
document::{ColorSwatchesId, DocumentColorSwatches, DocumentInlayHints, DocumentInlayHintsId},
editor::Action,
handlers::lsp::SignatureHelpInvoked,
theme::Style,
Expand Down Expand Up @@ -1245,18 +1247,29 @@ pub fn select_references_to_symbol_under_cursor(cx: &mut Context) {
);
}

pub fn compute_inlay_hints_for_all_views(editor: &mut Editor, jobs: &mut crate::job::Jobs) {
if !editor.config().lsp.display_inlay_hints {
pub fn compute_lsp_annotations_for_all_views(editor: &mut Editor, jobs: &mut crate::job::Jobs) {
let display_inlay_hints = editor.config().lsp.display_inlay_hints;
let display_color_swatches = editor.config().lsp.display_color_swatches;

if !display_inlay_hints && !display_color_swatches {
return;
}

for (view, _) in editor.tree.views() {
let doc = match editor.documents.get(&view.doc) {
Some(doc) => doc,
None => continue,
let Some(doc) = editor.documents.get(&view.doc) else {
continue;
};
if let Some(callback) = compute_inlay_hints_for_view(view, doc) {
jobs.callback(callback);

if display_inlay_hints {
if let Some(callback) = compute_inlay_hints_for_view(view, doc) {
jobs.callback(callback);
}
}

if display_color_swatches {
if let Some(callback) = compute_color_swatches_for_view(view, doc) {
jobs.callback(callback);
}
}
}
}
Expand All @@ -1272,20 +1285,7 @@ fn compute_inlay_hints_for_view(
.language_servers_with_feature(LanguageServerFeature::InlayHints)
.next()?;

let doc_text = doc.text();
let len_lines = doc_text.len_lines();

// Compute ~3 times the current view height of inlay hints, that way some scrolling
// will not show half the view with hints and half without while still being faster
// than computing all the hints for the full file (which could be dozens of time
// longer than the view is).
let view_height = view.inner_height();
let first_visible_line =
doc_text.char_to_line(doc.view_offset(view_id).anchor.min(doc_text.len_chars()));
let first_line = first_visible_line.saturating_sub(view_height);
let last_line = first_visible_line
.saturating_add(view_height.saturating_mul(2))
.min(len_lines);
let (first_line, last_line) = doc.inline_annotations_line_range(view.inner_height(), view.id);

let new_doc_inlay_hints_id = DocumentInlayHintsId {
first_line,
Expand All @@ -1300,6 +1300,7 @@ fn compute_inlay_hints_for_view(
return None;
}

let doc_text = doc.text();
let doc_slice = doc_text.slice(..);
let first_char_in_range = doc_slice.line_to_char(first_line);
let last_char_in_range = doc_slice.line_to_char(last_line);
Expand Down Expand Up @@ -1341,7 +1342,7 @@ fn compute_inlay_hints_for_view(

// Most language servers will already send them sorted but ensure this is the case to
// avoid errors on our end.
hints.sort_by_key(|inlay_hint| inlay_hint.position);
hints.sort_unstable_by_key(|inlay_hint| inlay_hint.position);

let mut padding_before_inlay_hints = Vec::new();
let mut type_inlay_hints = Vec::new();
Expand Down Expand Up @@ -1405,3 +1406,103 @@ fn compute_inlay_hints_for_view(

Some(callback)
}

fn compute_color_swatches_for_view(
view: &View,
doc: &Document,
) -> Option<std::pin::Pin<Box<impl Future<Output = Result<crate::job::Callback, anyhow::Error>>>>> {
let view_id = view.id;
let doc_id = view.doc;

let language_server = doc
.language_servers_with_feature(LanguageServerFeature::ColorProvider)
.next()?;
nik-rev marked this conversation as resolved.
Show resolved Hide resolved

let (first_line, last_line) = doc.inline_annotations_line_range(view.inner_height(), view.id);

let new_doc_color_swatches_id = ColorSwatchesId {
first_line,
last_line,
};

// Don't recompute the color swatches in case nothing has changed about the view
if !doc.color_swatches_outdated
&& doc
.color_swatches(view_id)
.is_some_and(|doc_color_swatches| doc_color_swatches.id == new_doc_color_swatches_id)
{
return None;
}

let offset_encoding = language_server.offset_encoding();

let callback = super::make_job_callback(
language_server.text_document_color_swatches(doc.identifier(), None)?,
nik-rev marked this conversation as resolved.
Show resolved Hide resolved
move |editor, _compositor, response: Option<Vec<lsp::ColorInformation>>| {
// The config was modified or the window was closed while the request was in flight
if !editor.config().lsp.display_color_swatches || editor.tree.try_get(view_id).is_none()
{
return;
}

// Add annotations to relevant document, not the current one (it may have changed in between)
let Some(doc) = editor.documents.get_mut(&doc_id) else {
return;
};

// If color swatches are empty or we don't have a response,
// empty the color swatches since they're now outdated
let mut swatches = match response {
Some(swatches) if !swatches.is_empty() => swatches,
_ => {
doc.set_color_swatches(
view_id,
DocumentColorSwatches::empty_with_id(new_doc_color_swatches_id),
);
return;
}
};

// Most language servers will already send them sorted but
// we ensure this is the case to avoid errors on our end.
swatches.sort_unstable_by_key(|swatch| swatch.range.start);

let swatch_count = swatches.len();

let mut color_swatches = Vec::with_capacity(swatch_count);
let mut color_swatches_padding = Vec::with_capacity(swatch_count);
let mut colors = Vec::with_capacity(swatch_count);

let doc_text = doc.text();

for swatch in swatches {
let Some(swatch_index) =
helix_lsp::util::lsp_pos_to_pos(doc_text, swatch.range.start, offset_encoding)
else {
// Skip color swatches that have no "real" position
continue;
};

color_swatches.push(vec![InlineAnnotation::new(swatch_index, "■")]);
color_swatches_padding.push(InlineAnnotation::new(swatch_index, " "));
colors.push(Highlight::Rgb(
(swatch.color.red * 255.) as u8,
(swatch.color.green * 255.) as u8,
(swatch.color.blue * 255.) as u8,
));
}

doc.set_color_swatches(
view_id,
DocumentColorSwatches {
id: new_doc_color_swatches_id,
colors,
color_swatches,
color_swatches_padding,
},
);
},
);

Some(callback)
}
6 changes: 5 additions & 1 deletion helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use super::*;

use helix_core::fuzzy::fuzzy_match;
use helix_core::indent::MAX_INDENT;
use helix_core::syntax::Highlight;
use helix_core::{line_ending, shellwords::Shellwords};
use helix_stdx::path::home_dir;
use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME};
Expand Down Expand Up @@ -1673,7 +1674,10 @@ fn tree_sitter_highlight_name(
return Ok(());
};

let content = cx.editor.theme.scope(highlight.0).to_string();
let content = match highlight {
Highlight::Indexed(idx) => cx.editor.theme.scope(idx).to_string(),
Highlight::Rgb(r, g, b) => format!("rgb({r}, {g}, {b})"),
};

let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new(
Expand Down
6 changes: 3 additions & 3 deletions helix-term/src/ui/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ impl<H: Iterator<Item = HighlightEvent>> Iterator for StyleIter<'_, H> {
let style = self
.active_highlights
.iter()
.fold(self.text_style, |acc, span| {
acc.patch(self.theme.highlight(span.0))
.fold(self.text_style, |acc, highlight| {
acc.patch(self.theme.highlight_to_style(*highlight))
});
if self.kind == StyleIterKind::BaseHighlights {
// Move the end byte index to the nearest character boundary (rounding up)
Expand Down Expand Up @@ -221,7 +221,7 @@ pub fn render_text(
let grapheme_style = if let GraphemeSource::VirtualText { highlight } = grapheme.source {
let mut style = renderer.text_style;
if let Some(highlight) = highlight {
style = style.patch(theme.highlight(highlight.0));
style = style.patch(theme.highlight_to_style(highlight));
}
GraphemeStyle {
syntax_style: style,
Expand Down
Loading
Loading