editor: Add document_colors LSP. (#1329)

This commit is contained in:
Jason Lee 2025-10-06 18:08:12 +08:00 committed by GitHub
parent efe7e14e88
commit d0396d779e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 507 additions and 109 deletions

199
Cargo.lock generated
View file

@ -484,6 +484,17 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@ -1159,6 +1170,18 @@ dependencies = [
"workspace-hack", "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]] [[package]]
name = "color_quant" name = "color_quant"
version = "1.1.0" version = "1.1.0"
@ -1466,6 +1489,15 @@ dependencies = [
"typenum", "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]] [[package]]
name = "cssparser" name = "cssparser"
version = "0.29.6" version = "0.29.6"
@ -1543,6 +1575,19 @@ dependencies = [
"syn 2.0.105", "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]] [[package]]
name = "data-url" name = "data-url"
version = "0.3.1" version = "0.3.1"
@ -2813,7 +2858,7 @@ dependencies = [
"html5ever 0.27.0", "html5ever 0.27.0",
"indoc", "indoc",
"itertools 0.13.0", "itertools 0.13.0",
"lsp-types", "lsp-types 0.97.0",
"markdown", "markdown",
"markup5ever_rcdom", "markup5ever_rcdom",
"notify", "notify",
@ -3249,7 +3294,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"wasm-bindgen", "wasm-bindgen",
"windows-core 0.58.0", "windows-core 0.61.2",
] ]
[[package]] [[package]]
@ -3874,6 +3919,19 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 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]] [[package]]
name = "lsp-types" name = "lsp-types"
version = "0.97.0" version = "0.97.0"
@ -4456,7 +4514,7 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
dependencies = [ dependencies = [
"proc-macro-crate 1.3.1", "proc-macro-crate 3.3.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.105", "syn 2.0.105",
@ -4870,7 +4928,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [ dependencies = [
"phf_macros", "phf_macros 0.8.0",
"phf_shared 0.8.0", "phf_shared 0.8.0",
"proc-macro-hack", "proc-macro-hack",
] ]
@ -4881,6 +4939,7 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [ dependencies = [
"phf_macros 0.11.3",
"phf_shared 0.11.3", "phf_shared 0.11.3",
] ]
@ -4938,6 +4997,19 @@ dependencies = [
"syn 1.0.109", "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]] [[package]]
name = "phf_shared" name = "phf_shared"
version = "0.8.0" version = "0.8.0"
@ -5729,7 +5801,7 @@ dependencies = [
"tokio-rustls", "tokio-rustls",
"tokio-socks", "tokio-socks",
"tokio-util", "tokio-util",
"tower", "tower 0.5.2",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
@ -6744,12 +6816,13 @@ dependencies = [
"anyhow", "anyhow",
"autocorrect", "autocorrect",
"chrono", "chrono",
"color-lsp",
"fake", "fake",
"gpui", "gpui",
"gpui-component", "gpui-component",
"gtk", "gtk",
"itertools 0.14.0", "itertools 0.14.0",
"lsp-types", "lsp-types 0.97.0",
"rand 0.8.5", "rand 0.8.5",
"raw-window-handle", "raw-window-handle",
"regex", "regex",
@ -7325,9 +7398,21 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"slab", "slab",
"socket2 0.6.0", "socket2 0.6.0",
"tokio-macros",
"windows-sys 0.59.0", "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]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.26.2" version = "0.26.2"
@ -7465,6 +7550,20 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" 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]] [[package]]
name = "tower" name = "tower"
version = "0.5.2" version = "0.5.2"
@ -7486,6 +7585,40 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 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]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.3" version = "0.3.3"
@ -8676,19 +8809,6 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.61.2" version = "0.61.2"
@ -8724,17 +8844,6 @@ dependencies = [
"syn 2.0.105", "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]] [[package]]
name = "windows-implement" name = "windows-implement"
version = "0.60.0" version = "0.60.0"
@ -8757,17 +8866,6 @@ dependencies = [
"syn 2.0.105", "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]] [[package]]
name = "windows-interface" name = "windows-interface"
version = "0.59.1" version = "0.59.1"
@ -8826,15 +8924,6 @@ dependencies = [
"windows-targets 0.52.6", "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]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.3.4" version = "0.3.4"
@ -8844,16 +8933,6 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "windows-strings" name = "windows-strings"
version = "0.3.1" version = "0.3.1"

View file

@ -26,6 +26,7 @@ tree-sitter-navi = "0.2.2"
itertools = "0.14.0" itertools = "0.14.0"
lsp-types.workspace = true lsp-types.workspace = true
autocorrect = "2.14.2" autocorrect = "2.14.2"
color-lsp = "0.2.0"
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
gtk = { version = "0.18" } gtk = { version = "0.18" }

View file

@ -14,8 +14,8 @@ use gpui_component::{
h_flex, h_flex,
highlighter::{Diagnostic, DiagnosticSeverity, Language, LanguageConfig, LanguageRegistry}, highlighter::{Diagnostic, DiagnosticSeverity, Language, LanguageConfig, LanguageRegistry},
input::{ input::{
self, CodeActionProvider, CompletionProvider, DefinitionProvider, HoverProvider, self, CodeActionProvider, CompletionProvider, DefinitionProvider, DocumentColorProvider,
InputEvent, InputState, Position, Rope, RopeExt, TabSize, TextInput, HoverProvider, InputEvent, InputState, Position, Rope, RopeExt, TabSize, TextInput,
}, },
v_flex, ActiveTheme, ContextModal, IconName, IndexPath, Selectable, Sizable, 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<gpui::Result<Vec<lsp_types::ColorInformation>>> {
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::<Vec<_>>();
Task::ready(Ok(colors))
}
}
impl Example { impl Example {
pub fn new(default: Option<String>, window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(default: Option<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let default_language = if let Some(name) = default { 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.code_action_providers = vec![lsp_store.clone(), Rc::new(TextConvertor)];
editor.lsp.hover_provider = Some(lsp_store.clone()); editor.lsp.hover_provider = Some(lsp_store.clone());
editor.lsp.definition_provider = Some(lsp_store.clone()); editor.lsp.definition_provider = Some(lsp_store.clone());
editor.lsp.document_color_provider = Some(lsp_store.clone());
editor editor
}); });

View file

@ -5,6 +5,10 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"keywords": ["UI", "desktop", "application"], "keywords": ["UI", "desktop", "application"],
"stars": 3000, "stars": 3000,
"rgb": "rgb(100, 200, 100)",
"hsla": "hsla(20, 100%, 50%, .5)",
"hsl": "hsl(225, 100%, 70%)",
"中文": "#EEAAFF",
"public": true, "public": true,
"repository": "https://github.com/longbridge/gpui-component" "repository": "https://github.com/longbridge/gpui-component"
} }

View file

@ -30,6 +30,8 @@ pub enum HelloError {
Timeout, Timeout,
} }
// Document colors: #FF0033, #00AA33, #0033FF, #FFAA33
type Result<T> = std::result::Result<T, HelloError>; type Result<T> = std::result::Result<T, HelloError>;
impl HelloWorld { impl HelloWorld {

View file

@ -2,19 +2,15 @@ use std::{ops::Range, rc::Rc};
use gpui::{ use gpui::{
fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler, fill, point, px, relative, size, App, Bounds, Corners, Element, ElementId, ElementInputHandler,
Entity, GlobalElementId, Half, HighlightStyle, Hitbox, IntoElement, LayoutId, MouseButton, Entity, GlobalElementId, Half, HighlightStyle, Hitbox, Hsla, IntoElement, LayoutId,
MouseMoveEvent, Path, Pixels, Point, ShapedLine, SharedString, Size, Style, TextRun, TextStyle, MouseButton, MouseMoveEvent, Path, Pixels, Point, ShapedLine, SharedString, Size, Style,
UnderlineStyle, Window, TextRun, TextStyle, UnderlineStyle, Window,
}; };
use ropey::Rope; use ropey::Rope;
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{ use crate::{
input::{ input::{blink_cursor::CURSOR_WIDTH, text_wrapper::LineLayout, RopeExt as _},
blink_cursor::CURSOR_WIDTH,
text_wrapper::{LineLayout, TextWrapper},
RopeExt as _,
},
ActiveTheme as _, Colorize, PixelsExt, Root, ActiveTheme as _, Colorize, PixelsExt, Root,
}; };
@ -241,7 +237,7 @@ impl TextElement {
pub(crate) fn layout_match_range( pub(crate) fn layout_match_range(
range: Range<usize>, range: Range<usize>,
last_layout: &LastLayout, last_layout: &LastLayout,
bounds: &mut Bounds<Pixels>, bounds: &Bounds<Pixels>,
) -> Option<Path<Pixels>> { ) -> Option<Path<Pixels>> {
if range.is_empty() { if range.is_empty() {
return None; return None;
@ -374,7 +370,7 @@ impl TextElement {
fn layout_search_matches( fn layout_search_matches(
&self, &self,
last_layout: &LastLayout, last_layout: &LastLayout,
bounds: &mut Bounds<Pixels>, bounds: &Bounds<Pixels>,
cx: &mut App, cx: &mut App,
) -> Vec<(Path<Pixels>, bool)> { ) -> Vec<(Path<Pixels>, bool)> {
let search_panel = self.state.read(cx).search_panel.clone(); let search_panel = self.state.read(cx).search_panel.clone();
@ -401,7 +397,7 @@ impl TextElement {
fn layout_hover_highlight( fn layout_hover_highlight(
&self, &self,
last_layout: &LastLayout, last_layout: &LastLayout,
bounds: &mut Bounds<Pixels>, bounds: &Bounds<Pixels>,
cx: &mut App, cx: &mut App,
) -> Option<Path<Pixels>> { ) -> Option<Path<Pixels>> {
let hover_popover = self.state.read(cx).hover_popover.clone(); 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) Self::layout_match_range(symbol_range, last_layout, bounds)
} }
fn layout_document_colors(
&self,
document_colors: &[(Range<usize>, Hsla)],
last_layout: &LastLayout,
bounds: &Bounds<Pixels>,
) -> Vec<(Path<Pixels>, 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( fn layout_selections(
&self, &self,
last_layout: &LastLayout, last_layout: &LastLayout,
@ -530,13 +542,16 @@ impl TextElement {
fn layout_lines( fn layout_lines(
state: &InputState, state: &InputState,
display_text: &Rope, display_text: &Rope,
text_wrapper: &TextWrapper, last_layout: &LastLayout,
visible_range: &Range<usize>,
font_size: Pixels, font_size: Pixels,
runs: &[TextRun], runs: &[TextRun],
bg_segments: &[(Range<usize>, Hsla)],
window: &mut Window, window: &mut Window,
) -> Vec<LineLayout> { ) -> Vec<LineLayout> {
let is_multi_line = state.mode.is_multi_line(); 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 { if !is_multi_line {
let shaped_line = window.text_system().shape_line( let shaped_line = window.text_system().shape_line(
@ -585,6 +600,16 @@ impl TextElement {
for range in &line_item.wrapped_lines { for range in &line_item.wrapped_lines {
let line_runs = runs_for_range(runs, offset, &range); 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 sub_line: SharedString = line[range.clone()].to_string().into();
let shaped_line = window let shaped_line = window
.text_system() .text_system()
@ -671,6 +696,7 @@ pub(super) struct PrepaintState {
selection_path: Option<Path<Pixels>>, selection_path: Option<Path<Pixels>>,
hover_highlight_path: Option<Path<Pixels>>, hover_highlight_path: Option<Path<Pixels>>,
search_match_paths: Vec<(Path<Pixels>, bool)>, search_match_paths: Vec<(Path<Pixels>, bool)>,
document_color_paths: Vec<(Path<Pixels>, Hsla)>,
hover_definition_hitbox: Option<Hitbox>, hover_definition_hitbox: Option<Hitbox>,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
} }
@ -812,6 +838,23 @@ impl Element for TextElement {
let (line_number_width, line_number_len) = let (line_number_width, line_number_len) =
Self::layout_line_numbers(&state, &text, font_size, &text_style, window); 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 { let run = TextRun {
len: display_text.len(), len: display_text.len(),
font: style.font(), font: style.font(),
@ -880,19 +923,16 @@ impl Element for TextElement {
vec![run] vec![run]
}; };
let wrap_width = if multi_line && state.soft_wrap { let document_colors = state
Some(bounds.size.width - line_number_width - RIGHT_MARGIN) .lsp
} else { .document_colors_for_range(&text, &last_layout.visible_range);
None
};
let lines = Self::layout_lines( let lines = Self::layout_lines(
&state, &state,
&display_text, &display_text,
&state.text_wrapper, &last_layout,
&visible_range,
font_size, font_size,
&runs, &runs,
&document_colors,
window, window,
); );
@ -917,6 +957,7 @@ impl Element for TextElement {
) )
.width; .width;
} }
last_layout.lines = Rc::new(lines);
let total_wrapped_lines = state.text_wrapper.len(); let total_wrapped_lines = state.text_wrapper.len();
let empty_bottom_height = bounds let empty_bottom_height = bounds
@ -934,17 +975,6 @@ impl Element for TextElement {
.max(bounds.size.height), .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 // `position_for_index` for example
// //
// #### text // #### text
@ -983,6 +1013,8 @@ impl Element for TextElement {
let search_match_paths = self.layout_search_matches(&last_layout, &mut bounds, cx); 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 selection_path = self.layout_selections(&last_layout, &mut bounds, cx);
let hover_highlight_path = self.layout_hover_highlight(&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 state = self.state.read(cx);
let line_numbers = if state.mode.line_number() { let line_numbers = if state.mode.line_number() {
@ -1045,6 +1077,7 @@ impl Element for TextElement {
search_match_paths, search_match_paths,
hover_highlight_path, hover_highlight_path,
hover_definition_hitbox, 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 // Paint text
let mut offset_y = mask_offset_y + invisible_top_padding; let mut offset_y = mask_offset_y + invisible_top_padding;
for line in prepaint.last_layout.lines.iter() { for line in prepaint.last_layout.lines.iter() {
@ -1191,11 +1229,7 @@ impl Element for TextElement {
input_bounds.size.height, input_bounds.size.height,
), ),
}, },
cx.theme() cx.theme().editor_background(),
.highlight_theme
.style
.editor_background
.unwrap_or(cx.theme().background),
)); ));
// Each item is the normal lines. // Each item is the normal lines.
@ -1285,6 +1319,68 @@ pub(super) fn runs_for_range(
result result
} }
fn split_runs_by_bg_segments(
start_offset: usize,
runs: &[TextRun],
bg_segments: &[(Range<usize>, Hsla)],
) -> Vec<TextRun> {
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; 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, 3, &(2..10)), &[4, 1, 3]);
assert_runs(runs_for_range(&runs, 9, &(0..8)), &[1, 7]); 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<_>>(),
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());
}
} }

View file

@ -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<Result<Vec<ColorInformation>>>;
}
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<usize>,
) -> Vec<(Range<usize>, 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<InputState>,
) {
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(())
});
}
}

View file

@ -1,5 +1,6 @@
use anyhow::Result; 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 std::rc::Rc;
use crate::input::{popovers::ContextMenu, InputState, RopeExt}; use crate::input::{popovers::ContextMenu, InputState, RopeExt};
@ -7,11 +8,13 @@ use crate::input::{popovers::ContextMenu, InputState, RopeExt};
mod code_actions; mod code_actions;
mod completions; mod completions;
mod definitions; mod definitions;
mod document_colors;
mod hover; mod hover;
pub use code_actions::*; pub use code_actions::*;
pub use completions::*; pub use completions::*;
pub use definitions::*; pub use definitions::*;
pub use document_colors::*;
pub use hover::*; pub use hover::*;
/// LSP ServerCapabilities /// LSP ServerCapabilities
@ -26,7 +29,12 @@ pub struct Lsp {
pub hover_provider: Option<Rc<dyn HoverProvider>>, pub hover_provider: Option<Rc<dyn HoverProvider>>,
/// The definition provider. /// The definition provider.
pub definition_provider: Option<Rc<dyn DefinitionProvider>>, pub definition_provider: Option<Rc<dyn DefinitionProvider>>,
/// The document color provider.
pub document_color_provider: Option<Rc<dyn DocumentColorProvider>>,
document_colors: Vec<(lsp_types::Range, Hsla)>,
_hover_task: Task<Result<()>>, _hover_task: Task<Result<()>>,
_document_color_task: Task<Result<()>>,
} }
impl Default for Lsp { impl Default for Lsp {
@ -36,11 +44,33 @@ impl Default for Lsp {
code_action_providers: vec![], code_action_providers: vec![],
hover_provider: None, hover_provider: None,
definition_provider: None, definition_provider: None,
document_color_provider: None,
document_colors: vec![],
_hover_task: Task::ready(Ok(())), _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<InputState>,
) {
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 { impl InputState {
pub(crate) fn hide_context_menu(&mut self, cx: &mut Context<Self>) { pub(crate) fn hide_context_menu(&mut self, cx: &mut Context<Self>) {
self.context_menu = None; self.context_menu = None;

View file

@ -323,6 +323,10 @@ pub struct InputState {
pub lsp: Lsp, 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. /// A flag to indicate if we should ignore the next completion event.
pub(super) silent_replace_text: bool, pub(super) silent_replace_text: bool,
@ -415,6 +419,7 @@ impl InputState {
silent_replace_text: false, silent_replace_text: false,
_subscriptions, _subscriptions,
_context_menu_task: Task::ready(Ok(())), _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(); self.selected_range = (self.text.len()..self.text.len()).into();
} else { } else {
self.selected_range.clear(); self.selected_range.clear();
self._pending_update = true;
self.lsp.reset();
} }
// Move scroll to top // Move scroll to top
self.scroll_handle.set_offset(point(px(0.), px(0.))); self.scroll_handle.set_offset(point(px(0.), px(0.)));
@ -799,6 +807,7 @@ impl InputState {
diagnostics.reset(&self.text) diagnostics.reset(&self.text)
} }
self.text_wrapper.set_default_text(&self.text); self.text_wrapper.set_default_text(&self.text);
self._pending_update = true;
self self
} }
@ -2145,6 +2154,7 @@ impl EntityInputHandler for InputState {
.update(&self.text, &range, &Rope::from(new_text), cx); .update(&self.text, &range, &Rope::from(new_text), cx);
self.mode self.mode
.update_highlighter(&range, &self.text, &new_text, true, cx); .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.selected_range = (new_offset..new_offset).into();
self.ime_marked_range.take(); self.ime_marked_range.take();
self.update_preferred_column(); self.update_preferred_column();
@ -2163,13 +2173,15 @@ impl EntityInputHandler for InputState {
range_utf16: Option<Range<usize>>, range_utf16: Option<Range<usize>>,
new_text: &str, new_text: &str,
new_selected_range_utf16: Option<Range<usize>>, new_selected_range_utf16: Option<Range<usize>>,
_: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if self.disabled { if self.disabled {
return; return;
} }
self.lsp.reset();
let range = range_utf16 let range = range_utf16
.as_ref() .as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16)) .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); .update(&self.text, &range, &Rope::from(new_text), cx);
self.mode self.mode
.update_highlighter(&range, &self.text, &new_text, true, cx); .update_highlighter(&range, &self.text, &new_text, true, cx);
self.lsp.update(&self.text, window, cx);
if new_text.is_empty() { if new_text.is_empty() {
// Cancel selection, when cancel IME input. // Cancel selection, when cancel IME input.
self.selected_range = (range.start..range.start).into(); self.selected_range = (range.start..range.start).into();
@ -2300,9 +2313,13 @@ impl Focusable for InputState {
} }
impl Render for InputState { impl Render for InputState {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
self.mode if self._pending_update {
.update_highlighter(&(0..0), &self.text, "", false, cx); self.mode
.update_highlighter(&(0..0), &self.text, "", false, cx);
self.lsp.update(&self.text, window, cx);
self._pending_update = false;
}
div() div()
.id("input-state") .id("input-state")

View file

@ -249,11 +249,7 @@ impl RenderOnce for TextInput {
cx.theme().muted cx.theme().muted
} else { } else {
if state.mode.is_code_editor() { if state.mode.is_code_editor() {
cx.theme() cx.theme().editor_background()
.highlight_theme
.style
.editor_background
.unwrap_or(cx.theme().background)
} else { } else {
cx.theme().background cx.theme().background
} }

View file

@ -168,6 +168,15 @@ impl Theme {
window.refresh(); 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<ThemeColor> for Theme { impl From<ThemeColor> for Theme {