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 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1c3caf5
feat: basic implementation
NikitaRevenco Dec 19, 2024
c901c17
feat: use different color
NikitaRevenco Dec 19, 2024
9185448
feat: remove unneeded comment
NikitaRevenco Dec 19, 2024
68c52a2
hack the text_decorations to make this work
gabydd Dec 20, 2024
cc71969
propogate the colours
gabydd Dec 20, 2024
0b71472
refactor: compute_lines function
NikitaRevenco Dec 20, 2024
0ce04ea
feat: move inlay_hints computations earlier
NikitaRevenco Dec 20, 2024
0c50ce1
refactor: move inlay hints computation function earlier
NikitaRevenco Dec 20, 2024
3f8731f
refactor: extract ColorSwatch into a separate struct
NikitaRevenco Dec 20, 2024
0f6e008
style: final changes, moving around structs
NikitaRevenco Dec 20, 2024
105be47
refactor: remove unused imports
NikitaRevenco Dec 20, 2024
857467f
feat: add space
NikitaRevenco Dec 20, 2024
253bdef
perf: use `with_capacity` since we know size of the vec
NikitaRevenco Dec 20, 2024
fbdfe74
docs: mention `display-color-swatches`
NikitaRevenco Dec 20, 2024
5f59c19
refactor: merge into one statement
NikitaRevenco Dec 20, 2024
54ff7a5
refactor: check for is_none
NikitaRevenco Dec 23, 2024
6db63d4
refactor: use let else statement
NikitaRevenco Dec 23, 2024
c7520c3
refactor: rename variable
NikitaRevenco Dec 23, 2024
cd9512c
refactor: use let else statement
NikitaRevenco Dec 23, 2024
ca5c96b
refactor: use let else statement
NikitaRevenco Dec 23, 2024
d120a35
feat: use padding for color swatches
NikitaRevenco Dec 23, 2024
7c40fbf
chore: apply suggestion from clippy lint
NikitaRevenco Dec 23, 2024
74b5705
perf: use a single iteration over the views of a document
NikitaRevenco Dec 25, 2024
db8ac50
refactor: rename several variables
NikitaRevenco Dec 25, 2024
67552f6
refactor: rename function
NikitaRevenco Dec 25, 2024
aca9b4f
refactor: inline_annotations_range moved to be on Document
NikitaRevenco Dec 25, 2024
ffb2ae3
docs: improve description of what the calculate range inline method does
NikitaRevenco Dec 25, 2024
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 @@ -146,6 +146,7 @@ The following statusline elements can be configured:
| `display-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
2 changes: 2 additions & 0 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
24 changes: 24 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,29 @@ 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>>> {
match self.capabilities.get().unwrap().color_provider {
Some(_) => (),
_ => return None,
}
NikitaRevenco marked this conversation as resolved.
Show resolved Hide resolved

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
156 changes: 140 additions & 16 deletions helix-term/src/commands/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ use helix_core::{
};
use helix_stdx::path;
use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId},
document::{ColorSwatchesId, DocumentColorSwatches, DocumentInlayHints, DocumentInlayHintsId},
editor::Action,
handlers::lsp::SignatureHelpInvoked,
theme::Style,
theme::{Color, Style},
Document, View,
};

Expand Down Expand Up @@ -1254,6 +1254,25 @@ pub fn compute_inlay_hints_for_all_views(editor: &mut Editor, jobs: &mut crate::
}
}

fn lsp_annotations_area(view: &View, doc: &Document) -> (usize, usize) {
// Compute ~3 times the current view heigh, that way some scrolling
NikitaRevenco marked this conversation as resolved.
Show resolved Hide resolved
// will not show half the view with annoations half without while still being faster
// than computing all the hints for the full file
let doc_text = doc.text();
let len_lines = doc_text.len_lines();

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);

(first_line, last_line)
}

fn compute_inlay_hints_for_view(
view: &View,
doc: &Document,
Expand All @@ -1265,20 +1284,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) = lsp_annotations_area(view, doc);

let new_doc_inlay_hints_id = DocumentInlayHintsId {
first_line,
Expand All @@ -1293,6 +1299,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 @@ -1398,3 +1405,120 @@ fn compute_inlay_hints_for_view(

Some(callback)
}

pub fn compute_color_swatches_for_all_views(editor: &mut Editor, jobs: &mut crate::job::Jobs) {
if !editor.config().lsp.display_color_swatches {
return;
}

for (view, _) in editor.tree.views() {
let doc = match editor.documents.get(&view.doc) {
Some(doc) => doc,
None => continue,
};
NikitaRevenco marked this conversation as resolved.
Show resolved Hide resolved
if let Some(callback) = compute_color_swatches_for_view(view, doc) {
jobs.callback(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()?;
NikitaRevenco marked this conversation as resolved.
Show resolved Hide resolved

let (first_line, last_line) = lsp_annotations_area(view, doc);

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)
.map_or(false, |dih| dih.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)?,
NikitaRevenco 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 doc = match editor.documents.get_mut(&doc_id) {
Some(doc) => doc,
None => return,
};
NikitaRevenco marked this conversation as resolved.
Show resolved Hide resolved

// If we have neither color swatches nor an LSP, empty the color swatches since they're now oudated
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),
);
doc.color_swatches_outdated = false;
return;
}
};

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

let mut color_swatches = Vec::with_capacity(swatches.len());
let mut colors = Vec::with_capacity(swatches.len());

let doc_text = doc.text();

for swatch in swatches {
let char_idx = match helix_lsp::util::lsp_pos_to_pos(
doc_text,
swatch.range.start,
offset_encoding,
) {
Some(pos) => pos,
// Skip color swatches that have no "real" position
None => continue,
};
NikitaRevenco marked this conversation as resolved.
Show resolved Hide resolved

color_swatches.push(InlineAnnotation::new(char_idx, "■ "));
colors.push(Color::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,
},
);

doc.color_swatches_outdated = false;
},
);

Some(callback)
}
6 changes: 4 additions & 2 deletions helix-term/src/ui/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,8 @@ pub fn render_text(
.unwrap_or((Style::default(), usize::MAX));
}

let grapheme_style = if let GraphemeSource::VirtualText { highlight } = grapheme.source {
let mut 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));
Expand All @@ -231,7 +232,8 @@ pub fn render_text(
overlay_style: overlay_style_span.0,
}
};
decorations.decorate_grapheme(renderer, &grapheme);

decorations.decorate_grapheme(renderer, &grapheme, &mut grapheme_style.syntax_style);

let virt = grapheme.is_virtual();
let grapheme_width = renderer.draw_grapheme(
Expand Down
8 changes: 8 additions & 0 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};

use tui::{buffer::Buffer as Surface, text::Span};

use super::text_decorations::ColorSwatch;

pub struct EditorView {
pub keymaps: Keymaps,
on_next_key: Option<(OnKeyCallback, OnKeyCallbackKind)>,
Expand Down Expand Up @@ -205,6 +207,11 @@ impl EditorView {
inline_diagnostic_config,
config.end_of_line_diagnostics,
));
if let Some(swatches) = doc.color_swatches(view.id) {
for (swatch, color) in swatches.color_swatches.iter().zip(swatches.colors.iter()) {
decorations.add_decoration(ColorSwatch::new(*color, swatch.char_idx));
}
}
render_document(
surface,
inner,
Expand Down Expand Up @@ -1112,6 +1119,7 @@ impl EditorView {

pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
commands::compute_inlay_hints_for_all_views(cx.editor, cx.jobs);
commands::compute_color_swatches_for_all_views(cx.editor, cx.jobs);

EventResult::Ignored(None)
}
Expand Down
47 changes: 44 additions & 3 deletions helix-term/src/ui/text_decorations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use std::cmp::Ordering;

use helix_core::doc_formatter::FormattedGrapheme;
use helix_core::Position;
use helix_view::editor::CursorCache;
use helix_view::{
editor::CursorCache,
theme::{Color, Style},
};

use crate::ui::document::{LinePos, TextRenderer};

Expand Down Expand Up @@ -81,6 +84,7 @@ pub trait Decoration {
&mut self,
_renderer: &mut TextRenderer,
_grapheme: &FormattedGrapheme,
_style: &mut Style,
) -> usize {
usize::MAX
}
Expand Down Expand Up @@ -108,7 +112,12 @@ impl<'a> DecorationManager<'a> {
}
}

pub fn decorate_grapheme(&mut self, renderer: &mut TextRenderer, grapheme: &FormattedGrapheme) {
pub fn decorate_grapheme(
&mut self,
renderer: &mut TextRenderer,
grapheme: &FormattedGrapheme,
style: &mut Style,
) {
for (decoration, hook_char_idx) in &mut self.decorations {
loop {
match (*hook_char_idx).cmp(&grapheme.char_idx) {
Expand All @@ -117,7 +126,7 @@ impl<'a> DecorationManager<'a> {
*hook_char_idx = decoration.skip_concealed_anchor(grapheme.char_idx)
}
Ordering::Equal => {
*hook_char_idx = decoration.decorate_grapheme(renderer, grapheme)
*hook_char_idx = decoration.decorate_grapheme(renderer, grapheme, style)
}
Ordering::Greater => break,
}
Expand All @@ -144,6 +153,37 @@ impl<'a> DecorationManager<'a> {
}
}

pub struct ColorSwatch {
NikitaRevenco marked this conversation as resolved.
Show resolved Hide resolved
color: Color,
pos: usize,
}

impl ColorSwatch {
pub fn new(color: Color, pos: usize) -> Self {
ColorSwatch { color, pos }
}
}

impl Decoration for ColorSwatch {
fn decorate_grapheme(
&mut self,
_renderer: &mut TextRenderer,
_grapheme: &FormattedGrapheme,
style: &mut Style,
) -> usize {
style.fg = Some(self.color);
usize::MAX
}

fn reset_pos(&mut self, pos: usize) -> usize {
if self.pos >= pos {
self.pos
} else {
usize::MAX
}
}
}

/// Cursor rendering is done externally so all the cursor decoration
/// does is save the position of primary cursor
pub struct Cursor<'a> {
Expand All @@ -163,6 +203,7 @@ impl Decoration for Cursor<'_> {
&mut self,
renderer: &mut TextRenderer,
grapheme: &FormattedGrapheme,
_style: &mut Style,
) -> usize {
if renderer.column_in_bounds(grapheme.visual_pos.col, grapheme.width())
&& renderer.offset.row < grapheme.visual_pos.row
Expand Down
1 change: 1 addition & 0 deletions helix-term/src/ui/text_decorations/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ impl Decoration for InlineDiagnostics<'_> {
&mut self,
renderer: &mut TextRenderer,
grapheme: &FormattedGrapheme,
_style: &mut Style,
) -> usize {
self.state
.proccess_anchor(grapheme, renderer.viewport.width, renderer.offset.col)
Expand Down
Loading
Loading