diff --git a/Cargo.lock b/Cargo.lock index 0343901f..51575a46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -484,6 +484,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -1159,6 +1170,18 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "color-lsp" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d4faf31b0bd306514c4ab7078cd992d611a79a5c8aad7a6e6538571f80f7eed" +dependencies = [ + "csscolorparser", + "indoc", + "tokio", + "tower-lsp", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -1466,6 +1489,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "csscolorparser" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda6aace1fbef3aa217b27f4c8d7d071ef2a70a5ca51050b1f17d40299d3f16" +dependencies = [ + "phf 0.11.3", +] + [[package]] name = "cssparser" version = "0.29.6" @@ -1543,6 +1575,19 @@ dependencies = [ "syn 2.0.105", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-url" version = "0.3.1" @@ -2813,7 +2858,7 @@ dependencies = [ "html5ever 0.27.0", "indoc", "itertools 0.13.0", - "lsp-types", + "lsp-types 0.97.0", "markdown", "markup5ever_rcdom", "notify", @@ -3249,7 +3294,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.58.0", + "windows-core 0.61.2", ] [[package]] @@ -3874,6 +3919,19 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lsp-types" +version = "0.94.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + [[package]] name = "lsp-types" version = "0.97.0" @@ -4456,7 +4514,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.3.0", "proc-macro2", "quote", "syn 2.0.105", @@ -4870,7 +4928,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -4881,6 +4939,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ + "phf_macros 0.11.3", "phf_shared 0.11.3", ] @@ -4938,6 +4997,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -5729,7 +5801,7 @@ dependencies = [ "tokio-rustls", "tokio-socks", "tokio-util", - "tower", + "tower 0.5.2", "tower-service", "url", "wasm-bindgen", @@ -6744,12 +6816,13 @@ dependencies = [ "anyhow", "autocorrect", "chrono", + "color-lsp", "fake", "gpui", "gpui-component", "gtk", "itertools 0.14.0", - "lsp-types", + "lsp-types 0.97.0", "rand 0.8.5", "raw-window-handle", "regex", @@ -7325,9 +7398,21 @@ dependencies = [ "pin-project-lite", "slab", "socket2 0.6.0", + "tokio-macros", "windows-sys 0.59.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "tokio-rustls" version = "0.26.2" @@ -7465,6 +7550,20 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower" version = "0.5.2" @@ -7486,6 +7585,40 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +[[package]] +name = "tower-lsp" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508" +dependencies = [ + "async-trait", + "auto_impl", + "bytes", + "dashmap", + "futures", + "httparse", + "lsp-types 0.94.1", + "memchr", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tower 0.4.13", + "tower-lsp-macros", + "tracing", +] + +[[package]] +name = "tower-lsp-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.105", +] + [[package]] name = "tower-service" version = "0.3.3" @@ -8676,19 +8809,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.61.2" @@ -8724,17 +8844,6 @@ dependencies = [ "syn 2.0.105", ] -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.105", -] - [[package]] name = "windows-implement" version = "0.60.0" @@ -8757,17 +8866,6 @@ dependencies = [ "syn 2.0.105", ] -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.105", -] - [[package]] name = "windows-interface" version = "0.59.1" @@ -8826,15 +8924,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -8844,16 +8933,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-strings" version = "0.3.1" diff --git a/crates/story/Cargo.toml b/crates/story/Cargo.toml index 65aef088..b94a7f4f 100644 --- a/crates/story/Cargo.toml +++ b/crates/story/Cargo.toml @@ -26,6 +26,7 @@ tree-sitter-navi = "0.2.2" itertools = "0.14.0" lsp-types.workspace = true autocorrect = "2.14.2" +color-lsp = "0.2.0" [target.'cfg(target_os = "linux")'.dependencies] gtk = { version = "0.18" } diff --git a/crates/story/examples/editor.rs b/crates/story/examples/editor.rs index be0d0acc..9a9716b9 100644 --- a/crates/story/examples/editor.rs +++ b/crates/story/examples/editor.rs @@ -14,8 +14,8 @@ use gpui_component::{ h_flex, highlighter::{Diagnostic, DiagnosticSeverity, Language, LanguageConfig, LanguageRegistry}, input::{ - self, CodeActionProvider, CompletionProvider, DefinitionProvider, HoverProvider, - InputEvent, InputState, Position, Rope, RopeExt, TabSize, TextInput, + self, CodeActionProvider, CompletionProvider, DefinitionProvider, DocumentColorProvider, + HoverProvider, InputEvent, InputState, Position, Rope, RopeExt, TabSize, TextInput, }, v_flex, ActiveTheme, ContextModal, IconName, IndexPath, Selectable, Sizable, }; @@ -619,6 +619,39 @@ impl CodeActionProvider for TextConvertor { } } +impl DocumentColorProvider for ExampleLspStore { + fn document_colors( + &self, + text: &Rope, + _window: &mut Window, + _cx: &mut App, + ) -> Task>> { + let nodes = color_lsp::parse(&text.to_string()); + let colors = nodes + .into_iter() + .map(|node| { + let start = lsp_types::Position::new(node.position.line, node.position.character); + let end = lsp_types::Position::new( + node.position.line, + node.position.character + node.matched.chars().count() as u32, + ); + + lsp_types::ColorInformation { + range: lsp_types::Range { start, end }, + color: lsp_types::Color { + red: node.color.r, + green: node.color.g, + blue: node.color.b, + alpha: node.color.a, + }, + } + }) + .collect::>(); + + Task::ready(Ok(colors)) + } +} + impl Example { pub fn new(default: Option, window: &mut Window, cx: &mut Context) -> Self { let default_language = if let Some(name) = default { @@ -650,6 +683,7 @@ impl Example { editor.lsp.code_action_providers = vec![lsp_store.clone(), Rc::new(TextConvertor)]; editor.lsp.hover_provider = Some(lsp_store.clone()); editor.lsp.definition_provider = Some(lsp_store.clone()); + editor.lsp.document_color_provider = Some(lsp_store.clone()); editor }); diff --git a/crates/story/examples/fixtures/test.json b/crates/story/examples/fixtures/test.json index 41f175bc..cecdbde7 100644 --- a/crates/story/examples/fixtures/test.json +++ b/crates/story/examples/fixtures/test.json @@ -5,6 +5,10 @@ "license": "Apache-2.0", "keywords": ["UI", "desktop", "application"], "stars": 3000, + "rgb": "rgb(100, 200, 100)", + "hsla": "hsla(20, 100%, 50%, .5)", + "hsl": "hsl(225, 100%, 70%)", + "中文": "#EEAAFF", "public": true, "repository": "https://github.com/longbridge/gpui-component" } diff --git a/crates/story/examples/fixtures/test.rs b/crates/story/examples/fixtures/test.rs index 63b34e89..af660ee1 100644 --- a/crates/story/examples/fixtures/test.rs +++ b/crates/story/examples/fixtures/test.rs @@ -30,6 +30,8 @@ pub enum HelloError { Timeout, } +// Document colors: #FF0033, #00AA33, #0033FF, #FFAA33 + type Result = std::result::Result; impl HelloWorld { diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index 7a39c92c..e11fa512 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -2,19 +2,15 @@ use std::{ops::Range, rc::Rc}; use gpui::{ fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler, - Entity, GlobalElementId, Half, HighlightStyle, Hitbox, IntoElement, LayoutId, MouseButton, - MouseMoveEvent, Path, Pixels, Point, ShapedLine, SharedString, Size, Style, TextRun, TextStyle, - UnderlineStyle, Window, + Entity, GlobalElementId, Half, HighlightStyle, Hitbox, Hsla, IntoElement, LayoutId, + MouseButton, MouseMoveEvent, Path, Pixels, Point, ShapedLine, SharedString, Size, Style, + TextRun, TextStyle, UnderlineStyle, Window, }; use ropey::Rope; use smallvec::SmallVec; use crate::{ - input::{ - blink_cursor::CURSOR_WIDTH, - text_wrapper::{LineLayout, TextWrapper}, - RopeExt as _, - }, + input::{blink_cursor::CURSOR_WIDTH, text_wrapper::LineLayout, RopeExt as _}, ActiveTheme as _, Colorize, PixelsExt, Root, }; @@ -241,7 +237,7 @@ impl TextElement { pub(crate) fn layout_match_range( range: Range, last_layout: &LastLayout, - bounds: &mut Bounds, + bounds: &Bounds, ) -> Option> { if range.is_empty() { return None; @@ -374,7 +370,7 @@ impl TextElement { fn layout_search_matches( &self, last_layout: &LastLayout, - bounds: &mut Bounds, + bounds: &Bounds, cx: &mut App, ) -> Vec<(Path, bool)> { let search_panel = self.state.read(cx).search_panel.clone(); @@ -401,7 +397,7 @@ impl TextElement { fn layout_hover_highlight( &self, last_layout: &LastLayout, - bounds: &mut Bounds, + bounds: &Bounds, cx: &mut App, ) -> Option> { let hover_popover = self.state.read(cx).hover_popover.clone(); @@ -413,6 +409,22 @@ impl TextElement { Self::layout_match_range(symbol_range, last_layout, bounds) } + fn layout_document_colors( + &self, + document_colors: &[(Range, Hsla)], + last_layout: &LastLayout, + bounds: &Bounds, + ) -> Vec<(Path, Hsla)> { + let mut paths = vec![]; + for (range, color) in document_colors.iter() { + if let Some(path) = Self::layout_match_range(range.clone(), last_layout, bounds) { + paths.push((path, *color)); + } + } + + paths + } + fn layout_selections( &self, last_layout: &LastLayout, @@ -530,13 +542,16 @@ impl TextElement { fn layout_lines( state: &InputState, display_text: &Rope, - text_wrapper: &TextWrapper, - visible_range: &Range, + last_layout: &LastLayout, font_size: Pixels, runs: &[TextRun], + bg_segments: &[(Range, Hsla)], window: &mut Window, ) -> Vec { let is_multi_line = state.mode.is_multi_line(); + let text_wrapper = &state.text_wrapper; + let visible_range = &last_layout.visible_range; + let visible_range_offset = &last_layout.visible_range_offset; if !is_multi_line { let shaped_line = window.text_system().shape_line( @@ -585,6 +600,16 @@ impl TextElement { for range in &line_item.wrapped_lines { let line_runs = runs_for_range(runs, offset, &range); + let line_runs = if bg_segments.is_empty() { + line_runs + } else { + split_runs_by_bg_segments( + visible_range_offset.start + offset, + &line_runs, + bg_segments, + ) + }; + let sub_line: SharedString = line[range.clone()].to_string().into(); let shaped_line = window .text_system() @@ -671,6 +696,7 @@ pub(super) struct PrepaintState { selection_path: Option>, hover_highlight_path: Option>, search_match_paths: Vec<(Path, bool)>, + document_color_paths: Vec<(Path, Hsla)>, hover_definition_hitbox: Option, bounds: Bounds, } @@ -812,6 +838,23 @@ impl Element for TextElement { let (line_number_width, line_number_len) = Self::layout_line_numbers(&state, &text, font_size, &text_style, window); + let wrap_width = if multi_line && state.soft_wrap { + Some(bounds.size.width - line_number_width - RIGHT_MARGIN) + } else { + None + }; + + let mut last_layout = LastLayout { + visible_range, + visible_top, + visible_range_offset: visible_start_offset..visible_end_offset, + line_height, + wrap_width, + line_number_width, + lines: Rc::new(vec![]), + cursor_bounds: None, + }; + let run = TextRun { len: display_text.len(), font: style.font(), @@ -880,19 +923,16 @@ impl Element for TextElement { vec![run] }; - let wrap_width = if multi_line && state.soft_wrap { - Some(bounds.size.width - line_number_width - RIGHT_MARGIN) - } else { - None - }; - + let document_colors = state + .lsp + .document_colors_for_range(&text, &last_layout.visible_range); let lines = Self::layout_lines( &state, &display_text, - &state.text_wrapper, - &visible_range, + &last_layout, font_size, &runs, + &document_colors, window, ); @@ -917,6 +957,7 @@ impl Element for TextElement { ) .width; } + last_layout.lines = Rc::new(lines); let total_wrapped_lines = state.text_wrapper.len(); let empty_bottom_height = bounds @@ -934,17 +975,6 @@ impl Element for TextElement { .max(bounds.size.height), ); - let mut last_layout = LastLayout { - visible_range, - visible_top, - visible_range_offset: visible_start_offset..visible_end_offset, - line_height, - wrap_width, - line_number_width, - lines: Rc::new(lines), - cursor_bounds: None, - }; - // `position_for_index` for example // // #### text @@ -983,6 +1013,8 @@ impl Element for TextElement { let search_match_paths = self.layout_search_matches(&last_layout, &mut bounds, cx); let selection_path = self.layout_selections(&last_layout, &mut bounds, cx); let hover_highlight_path = self.layout_hover_highlight(&last_layout, &mut bounds, cx); + let document_color_paths = + self.layout_document_colors(&document_colors, &last_layout, &bounds); let state = self.state.read(cx); let line_numbers = if state.mode.line_number() { @@ -1045,6 +1077,7 @@ impl Element for TextElement { search_match_paths, hover_highlight_path, hover_definition_hitbox, + document_color_paths, } } @@ -1158,6 +1191,11 @@ impl Element for TextElement { } } + // Paint document colors + for (path, color) in prepaint.document_color_paths.iter() { + window.paint_path(path.clone(), *color); + } + // Paint text let mut offset_y = mask_offset_y + invisible_top_padding; for line in prepaint.last_layout.lines.iter() { @@ -1191,11 +1229,7 @@ impl Element for TextElement { input_bounds.size.height, ), }, - cx.theme() - .highlight_theme - .style - .editor_background - .unwrap_or(cx.theme().background), + cx.theme().editor_background(), )); // Each item is the normal lines. @@ -1285,6 +1319,68 @@ pub(super) fn runs_for_range( result } +fn split_runs_by_bg_segments( + start_offset: usize, + runs: &[TextRun], + bg_segments: &[(Range, Hsla)], +) -> Vec { + let mut result = vec![]; + + let mut cursor = start_offset; + for run in runs { + let mut run_start = cursor; + let run_end = cursor + run.len; + + for (bg_range, bg_color) in bg_segments { + if run_end <= bg_range.start || run_start >= bg_range.end { + continue; + } + + // Overlap exists + if run_start < bg_range.start { + // Add the part before the background range + result.push(TextRun { + len: bg_range.start - run_start, + ..run.clone() + }); + } + + // Add the overlapping part with background color + let overlap_start = run_start.max(bg_range.start); + let overlap_end = run_end.min(bg_range.end); + let text_color = if bg_color.l >= 0.5 { + gpui::black() + } else { + gpui::white() + }; + + let run_len = overlap_end.saturating_sub(overlap_start); + if run_len > 0 { + result.push(TextRun { + len: run_len, + color: text_color, + ..run.clone() + }); + + cursor = bg_range.end; + run_start = cursor; + } + } + + if run_end > cursor { + // Add the part after the background range + result.push(TextRun { + len: run_end - cursor, + ..run.clone() + }); + } + + cursor = run_end; + } + + result +} + #[cfg(test)] mod tests { use super::*; @@ -1346,4 +1442,44 @@ mod tests { assert_runs(runs_for_range(&runs, 3, &(2..10)), &[4, 1, 3]); assert_runs(runs_for_range(&runs, 9, &(0..8)), &[1, 7]); } + + #[test] + fn test_split_runs_by_bg_segments() { + let run = TextRun { + len: 0, + font: gpui::font(".SystemUIFont"), + color: gpui::blue(), + background_color: None, + underline: None, + strikethrough: None, + }; + + let runs = vec![ + TextRun { + len: 5, + ..run.clone() + }, + TextRun { + len: 7, + ..run.clone() + }, + TextRun { + len: 24, + ..run.clone() + }, + ]; + + let bg_segments = vec![(8..12, gpui::red()), (12..18, gpui::blue())]; + let result = split_runs_by_bg_segments(5, &runs, &bg_segments); + assert_eq!( + result.iter().map(|run| run.len).collect::>(), + vec![3, 2, 2, 5, 1, 23] + ); + assert_eq!(result[0].color, gpui::blue()); + assert_eq!(result[1].color, gpui::black()); + assert_eq!(result[2].color, gpui::black()); + assert_eq!(result[3].color, gpui::black()); + assert_eq!(result[4].color, gpui::black()); + assert_eq!(result[5].color, gpui::blue()); + } } diff --git a/crates/ui/src/input/lsp/document_colors.rs b/crates/ui/src/input/lsp/document_colors.rs new file mode 100644 index 00000000..f5fb21b5 --- /dev/null +++ b/crates/ui/src/input/lsp/document_colors.rs @@ -0,0 +1,90 @@ +use std::ops::Range; + +use anyhow::Result; +use gpui::{App, Context, Hsla, Task, Window}; +use lsp_types::ColorInformation; +use ropey::Rope; + +use crate::input::{InputState, Lsp, RopeExt}; + +pub trait DocumentColorProvider { + /// Fetches document colors for the specified range. + /// + /// textDocument/documentColor + /// + /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentColor + fn document_colors( + &self, + _text: &Rope, + window: &mut Window, + cx: &mut App, + ) -> Task>>; +} + +impl Lsp { + /// Get document colors that intersect with the visible range (0-based row). + /// + /// Returns byte ranges and colors. + pub(crate) fn document_colors_for_range( + &self, + text: &Rope, + visible_range: &Range, + ) -> Vec<(Range, Hsla)> { + self.document_colors + .iter() + .filter_map(|(range, color)| { + if (range.start.line as usize) > visible_range.end + || (range.end.line as usize) < visible_range.start + { + return None; + } + + let start = text.position_to_offset(&range.start); + let end = text.position_to_offset(&range.end); + + Some((start..end, *color)) + }) + .collect() + } + pub(crate) fn update_document_colors( + &mut self, + text: &Rope, + window: &mut Window, + cx: &mut Context, + ) { + let Some(provider) = self.document_color_provider.as_ref() else { + return; + }; + + let task = provider.document_colors(text, window, cx); + self._hover_task = cx.spawn_in(window, async move |editor, cx| { + let colors = task.await?; + + editor.update(cx, |editor, cx| { + let mut document_colors: Vec<(lsp_types::Range, Hsla)> = colors + .iter() + .map(|info| { + let color = gpui::Rgba { + r: info.color.red, + g: info.color.green, + b: info.color.blue, + a: info.color.alpha, + } + .into(); + + (info.range, color) + }) + .collect(); + document_colors.sort_by_key(|(range, _)| range.start); + + if document_colors == editor.lsp.document_colors { + return; + } + editor.lsp.document_colors = document_colors; + cx.notify(); + })?; + + Ok(()) + }); + } +} diff --git a/crates/ui/src/input/lsp/mod.rs b/crates/ui/src/input/lsp/mod.rs index 5dd6fe88..6506cb16 100644 --- a/crates/ui/src/input/lsp/mod.rs +++ b/crates/ui/src/input/lsp/mod.rs @@ -1,5 +1,6 @@ use anyhow::Result; -use gpui::{App, Context, MouseMoveEvent, Task, Window}; +use gpui::{App, Context, Hsla, MouseMoveEvent, Task, Window}; +use ropey::Rope; use std::rc::Rc; use crate::input::{popovers::ContextMenu, InputState, RopeExt}; @@ -7,11 +8,13 @@ use crate::input::{popovers::ContextMenu, InputState, RopeExt}; mod code_actions; mod completions; mod definitions; +mod document_colors; mod hover; pub use code_actions::*; pub use completions::*; pub use definitions::*; +pub use document_colors::*; pub use hover::*; /// LSP ServerCapabilities @@ -26,7 +29,12 @@ pub struct Lsp { pub hover_provider: Option>, /// The definition provider. pub definition_provider: Option>, + /// The document color provider. + pub document_color_provider: Option>, + + document_colors: Vec<(lsp_types::Range, Hsla)>, _hover_task: Task>, + _document_color_task: Task>, } impl Default for Lsp { @@ -36,11 +44,33 @@ impl Default for Lsp { code_action_providers: vec![], hover_provider: None, definition_provider: None, + document_color_provider: None, + document_colors: vec![], _hover_task: Task::ready(Ok(())), + _document_color_task: Task::ready(Ok(())), } } } +impl Lsp { + /// Update the LSP when the text changes. + pub(crate) fn update( + &mut self, + text: &Rope, + window: &mut Window, + cx: &mut Context, + ) { + self.update_document_colors(text, window, cx); + } + + /// Reset all LSP states. + pub(crate) fn reset(&mut self) { + self.document_colors.clear(); + self._hover_task = Task::ready(Ok(())); + self._document_color_task = Task::ready(Ok(())); + } +} + impl InputState { pub(crate) fn hide_context_menu(&mut self, cx: &mut Context) { self.context_menu = None; diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 3277541b..b6c151d1 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -323,6 +323,10 @@ pub struct InputState { pub lsp: Lsp, + /// A flag to indicate if we have a pending update to the text. + /// + /// If true, will call some update (for example LSP, Syntax Highlight) before render. + _pending_update: bool, /// A flag to indicate if we should ignore the next completion event. pub(super) silent_replace_text: bool, @@ -415,6 +419,7 @@ impl InputState { silent_replace_text: false, _subscriptions, _context_menu_task: Task::ready(Ok(())), + _pending_update: false, } } @@ -640,6 +645,9 @@ impl InputState { self.selected_range = (self.text.len()..self.text.len()).into(); } else { self.selected_range.clear(); + + self._pending_update = true; + self.lsp.reset(); } // Move scroll to top self.scroll_handle.set_offset(point(px(0.), px(0.))); @@ -799,6 +807,7 @@ impl InputState { diagnostics.reset(&self.text) } self.text_wrapper.set_default_text(&self.text); + self._pending_update = true; self } @@ -2145,6 +2154,7 @@ impl EntityInputHandler for InputState { .update(&self.text, &range, &Rope::from(new_text), cx); self.mode .update_highlighter(&range, &self.text, &new_text, true, cx); + self.lsp.update(&self.text, window, cx); self.selected_range = (new_offset..new_offset).into(); self.ime_marked_range.take(); self.update_preferred_column(); @@ -2163,13 +2173,15 @@ impl EntityInputHandler for InputState { range_utf16: Option>, new_text: &str, new_selected_range_utf16: Option>, - _: &mut Window, + window: &mut Window, cx: &mut Context, ) { if self.disabled { return; } + self.lsp.reset(); + let range = range_utf16 .as_ref() .map(|range_utf16| self.range_from_utf16(range_utf16)) @@ -2198,6 +2210,7 @@ impl EntityInputHandler for InputState { .update(&self.text, &range, &Rope::from(new_text), cx); self.mode .update_highlighter(&range, &self.text, &new_text, true, cx); + self.lsp.update(&self.text, window, cx); if new_text.is_empty() { // Cancel selection, when cancel IME input. self.selected_range = (range.start..range.start).into(); @@ -2300,9 +2313,13 @@ impl Focusable for InputState { } impl Render for InputState { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - self.mode - .update_highlighter(&(0..0), &self.text, "", false, cx); + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + if self._pending_update { + self.mode + .update_highlighter(&(0..0), &self.text, "", false, cx); + self.lsp.update(&self.text, window, cx); + self._pending_update = false; + } div() .id("input-state") diff --git a/crates/ui/src/input/text_input.rs b/crates/ui/src/input/text_input.rs index 2c07d0d1..31f07296 100644 --- a/crates/ui/src/input/text_input.rs +++ b/crates/ui/src/input/text_input.rs @@ -249,11 +249,7 @@ impl RenderOnce for TextInput { cx.theme().muted } else { if state.mode.is_code_editor() { - cx.theme() - .highlight_theme - .style - .editor_background - .unwrap_or(cx.theme().background) + cx.theme().editor_background() } else { cx.theme().background } diff --git a/crates/ui/src/theme/mod.rs b/crates/ui/src/theme/mod.rs index abbc80b8..62c70491 100644 --- a/crates/ui/src/theme/mod.rs +++ b/crates/ui/src/theme/mod.rs @@ -168,6 +168,15 @@ impl Theme { window.refresh(); } } + + /// Get the editor background color, if not set, use the theme background color. + #[inline] + pub(crate) fn editor_background(&self) -> Hsla { + self.highlight_theme + .style + .editor_background + .unwrap_or(self.background) + } } impl From for Theme {