Detailed changes
@@ -1045,6 +1045,19 @@
// Automatically update Zed. This setting may be ignored on Linux if
// installed through a package manager.
"auto_update": true,
+ // How to render LSP `textDocument/documentColor` colors in the editor.
+ //
+ // Possible values:
+ //
+ // 1. Do not query and render document colors.
+ // "lsp_document_colors": "none",
+ // 2. Render document colors as inlay hints near the color text (default).
+ // "lsp_document_colors": "inlay",
+ // 3. Draw a border around the color text.
+ // "lsp_document_colors": "border",
+ // 4. Draw a background behind the color text..
+ // "lsp_document_colors": "background",
+ "lsp_document_colors": "inlay",
// Diagnostics configuration.
"diagnostics": {
// Whether to show the project diagnostics button in the status bar.
@@ -323,6 +323,7 @@ impl Server {
.add_request_handler(forward_read_only_project_request::<proto::SynchronizeBuffers>)
.add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
+ .add_request_handler(forward_read_only_project_request::<proto::GetColorPresentation>)
.add_request_handler(forward_mutating_project_request::<proto::GetCodeLens>)
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
.add_request_handler(forward_read_only_project_request::<proto::GitGetBranches>)
@@ -4,7 +4,7 @@ use crate::{
};
use call::ActiveCall;
use editor::{
- Editor, RowInfo,
+ DocumentColorsRenderMode, Editor, EditorSettings, RowInfo,
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
ExpandMacroRecursively, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
@@ -16,7 +16,7 @@ use editor::{
};
use fs::Fs;
use futures::StreamExt;
-use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
+use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
use indoc::indoc;
use language::{
FakeLspAdapter,
@@ -1951,6 +1951,283 @@ async fn test_inlay_hint_refresh_is_forwarded(
});
}
+#[gpui::test(iterations = 10)]
+async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+ let expected_color = Rgba {
+ r: 0.33,
+ g: 0.33,
+ b: 0.33,
+ a: 0.33,
+ };
+ let mut server = TestServer::start(cx_a.executor()).await;
+ let executor = cx_a.executor();
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ cx_a.update(|cx| {
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings::<EditorSettings>(cx, |settings| {
+ settings.lsp_document_colors = Some(DocumentColorsRenderMode::None);
+ });
+ });
+ });
+ cx_b.update(|cx| {
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings::<EditorSettings>(cx, |settings| {
+ settings.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
+ });
+ });
+ });
+
+ client_a.language_registry().add(rust_lang());
+ client_b.language_registry().add(rust_lang());
+ let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
+ "Rust",
+ FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
+ ..lsp::ServerCapabilities::default()
+ },
+ ..FakeLspAdapter::default()
+ },
+ );
+
+ // Client A opens a project.
+ client_a
+ .fs()
+ .insert_tree(
+ path!("/a"),
+ json!({
+ "main.rs": "fn main() { a }",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+
+ // Client B joins the project
+ let project_b = client_b.join_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
+ executor.start_waiting();
+
+ // The host opens a rust file.
+ let _buffer_a = project_a
+ .update(cx_a, |project, cx| {
+ project.open_local_buffer(path!("/a/main.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let editor_a = workspace_a
+ .update_in(cx_a, |workspace, window, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+
+ let requests_made = Arc::new(AtomicUsize::new(0));
+ let closure_requests_made = Arc::clone(&requests_made);
+ let mut color_request_handle = fake_language_server
+ .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
+ let requests_made = Arc::clone(&closure_requests_made);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ );
+ requests_made.fetch_add(1, atomic::Ordering::Release);
+ Ok(vec![lsp::ColorInformation {
+ range: lsp::Range {
+ start: lsp::Position {
+ line: 0,
+ character: 0,
+ },
+ end: lsp::Position {
+ line: 0,
+ character: 1,
+ },
+ },
+ color: lsp::Color {
+ red: 0.33,
+ green: 0.33,
+ blue: 0.33,
+ alpha: 0.33,
+ },
+ }])
+ }
+ });
+ executor.run_until_parked();
+
+ assert_eq!(
+ 0,
+ requests_made.load(atomic::Ordering::Acquire),
+ "Host did not enable document colors, hence should query for none"
+ );
+ editor_a.update(cx_a, |editor, cx| {
+ assert_eq!(
+ Vec::<Rgba>::new(),
+ extract_color_inlays(editor, cx),
+ "No query colors should result in no hints"
+ );
+ });
+
+ let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+ let editor_b = workspace_b
+ .update_in(cx_b, |workspace, window, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ color_request_handle.next().await.unwrap();
+ executor.run_until_parked();
+
+ assert_eq!(
+ 1,
+ requests_made.load(atomic::Ordering::Acquire),
+ "The client opened the file and got its first colors back"
+ );
+ editor_b.update(cx_b, |editor, cx| {
+ assert_eq!(
+ vec![expected_color],
+ extract_color_inlays(editor, cx),
+ "With document colors as inlays, color inlays should be pushed"
+ );
+ });
+
+ editor_a.update_in(cx_a, |editor, window, cx| {
+ editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
+ editor.handle_input(":", window, cx);
+ });
+ color_request_handle.next().await.unwrap();
+ executor.run_until_parked();
+ assert_eq!(
+ 2,
+ requests_made.load(atomic::Ordering::Acquire),
+ "After the host edits his file, the client should request the colors again"
+ );
+ editor_a.update(cx_a, |editor, cx| {
+ assert_eq!(
+ Vec::<Rgba>::new(),
+ extract_color_inlays(editor, cx),
+ "Host has no colors still"
+ );
+ });
+ editor_b.update(cx_b, |editor, cx| {
+ assert_eq!(vec![expected_color], extract_color_inlays(editor, cx),);
+ });
+
+ cx_b.update(|_, cx| {
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings::<EditorSettings>(cx, |settings| {
+ settings.lsp_document_colors = Some(DocumentColorsRenderMode::Background);
+ });
+ });
+ });
+ executor.run_until_parked();
+ assert_eq!(
+ 2,
+ requests_made.load(atomic::Ordering::Acquire),
+ "After the client have changed the colors settings, no extra queries should happen"
+ );
+ editor_a.update(cx_a, |editor, cx| {
+ assert_eq!(
+ Vec::<Rgba>::new(),
+ extract_color_inlays(editor, cx),
+ "Host is unaffected by the client's settings changes"
+ );
+ });
+ editor_b.update(cx_b, |editor, cx| {
+ assert_eq!(
+ Vec::<Rgba>::new(),
+ extract_color_inlays(editor, cx),
+ "Client should have no colors hints, as in the settings"
+ );
+ });
+
+ cx_b.update(|_, cx| {
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings::<EditorSettings>(cx, |settings| {
+ settings.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
+ });
+ });
+ });
+ executor.run_until_parked();
+ assert_eq!(
+ 2,
+ requests_made.load(atomic::Ordering::Acquire),
+ "After falling back to colors as inlays, no extra LSP queries are made"
+ );
+ editor_a.update(cx_a, |editor, cx| {
+ assert_eq!(
+ Vec::<Rgba>::new(),
+ extract_color_inlays(editor, cx),
+ "Host is unaffected by the client's settings changes, again"
+ );
+ });
+ editor_b.update(cx_b, |editor, cx| {
+ assert_eq!(
+ vec![expected_color],
+ extract_color_inlays(editor, cx),
+ "Client should have its color hints back"
+ );
+ });
+
+ cx_a.update(|_, cx| {
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings::<EditorSettings>(cx, |settings| {
+ settings.lsp_document_colors = Some(DocumentColorsRenderMode::Border);
+ });
+ });
+ });
+ color_request_handle.next().await.unwrap();
+ executor.run_until_parked();
+ assert_eq!(
+ 3,
+ requests_made.load(atomic::Ordering::Acquire),
+ "After the host enables document colors, another LSP query should be made"
+ );
+ editor_a.update(cx_a, |editor, cx| {
+ assert_eq!(
+ Vec::<Rgba>::new(),
+ extract_color_inlays(editor, cx),
+ "Host did not configure document colors as hints hence gets nothing"
+ );
+ });
+ editor_b.update(cx_b, |editor, cx| {
+ assert_eq!(
+ vec![expected_color],
+ extract_color_inlays(editor, cx),
+ "Client should be unaffected by the host's settings changes"
+ );
+ });
+}
+
#[gpui::test(iterations = 10)]
async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.executor()).await;
@@ -2834,6 +3111,16 @@ fn extract_hint_labels(editor: &Editor) -> Vec<String> {
labels
}
+#[track_caller]
+fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
+ editor
+ .all_inlays(cx)
+ .into_iter()
+ .filter_map(|inlay| inlay.get_color())
+ .map(Rgba::from)
+ .collect()
+}
+
fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
git::blame::BlameEntry {
sha: sha.parse().unwrap(),
@@ -1,7 +1,7 @@
use super::*;
use collections::{HashMap, HashSet};
use editor::{
- DisplayPoint, EditorSettings, InlayId,
+ DisplayPoint, EditorSettings,
actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning},
display_map::{DisplayRow, Inlay},
test::{
@@ -870,11 +870,11 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
editor.splice_inlays(
&[],
- vec![Inlay {
- id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
- position: snapshot.buffer_snapshot.anchor_before(position),
- text: Rope::from(format!("Test inlay {next_inlay_id}")),
- }],
+ vec![Inlay::inline_completion(
+ post_inc(&mut next_inlay_id),
+ snapshot.buffer_snapshot.anchor_before(position),
+ format!("Test inlay {next_inlay_id}"),
+ )],
cx,
);
}
@@ -2014,11 +2014,11 @@ pub mod tests {
map.update(cx, |map, cx| {
map.splice_inlays(
&[],
- vec![Inlay {
- id: InlayId::InlineCompletion(0),
- position: buffer_snapshot.anchor_after(0),
- text: "\n".into(),
- }],
+ vec![Inlay::inline_completion(
+ 0,
+ buffer_snapshot.anchor_after(0),
+ "\n",
+ )],
cx,
);
});
@@ -1,5 +1,6 @@
use crate::{HighlightStyles, InlayId};
use collections::BTreeSet;
+use gpui::{Hsla, Rgba};
use language::{Chunk, Edit, Point, TextSummary};
use multi_buffer::{
Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset,
@@ -39,6 +40,7 @@ pub struct Inlay {
pub id: InlayId,
pub position: Anchor,
pub text: text::Rope,
+ color: Option<Hsla>,
}
impl Inlay {
@@ -54,6 +56,26 @@ impl Inlay {
id: InlayId::Hint(id),
position,
text: text.into(),
+ color: None,
+ }
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn mock_hint(id: usize, position: Anchor, text: impl Into<Rope>) -> Self {
+ Self {
+ id: InlayId::Hint(id),
+ position,
+ text: text.into(),
+ color: None,
+ }
+ }
+
+ pub fn color(id: usize, position: Anchor, color: Rgba) -> Self {
+ Self {
+ id: InlayId::Color(id),
+ position,
+ text: Rope::from("◼"),
+ color: Some(Hsla::from(color)),
}
}
@@ -62,16 +84,23 @@ impl Inlay {
id: InlayId::InlineCompletion(id),
position,
text: text.into(),
+ color: None,
}
}
- pub fn debugger_hint<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
+ pub fn debugger<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
Self {
id: InlayId::DebuggerValue(id),
position,
text: text.into(),
+ color: None,
}
}
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn get_color(&self) -> Option<Hsla> {
+ self.color
+ }
}
impl sum_tree::Item for Transform {
@@ -296,6 +325,14 @@ impl<'a> Iterator for InlayChunks<'a> {
}
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
+ InlayId::Color(_) => match inlay.color {
+ Some(color) => {
+ let style = self.highlight_styles.inlay_hint.get_or_insert_default();
+ style.color = Some(color);
+ Some(*style)
+ }
+ None => self.highlight_styles.inlay_hint,
+ },
};
let next_inlay_highlight_endpoint;
let offset_in_inlay = self.output_offset - self.transforms.start().0;
@@ -634,24 +671,24 @@ impl InlayMap {
.take(len)
.collect::<String>();
- let inlay_id = if i % 2 == 0 {
- InlayId::Hint(post_inc(next_inlay_id))
+ let next_inlay = if i % 2 == 0 {
+ Inlay::mock_hint(
+ post_inc(next_inlay_id),
+ snapshot.buffer.anchor_at(position, bias),
+ text.clone(),
+ )
} else {
- InlayId::InlineCompletion(post_inc(next_inlay_id))
+ Inlay::inline_completion(
+ post_inc(next_inlay_id),
+ snapshot.buffer.anchor_at(position, bias),
+ text.clone(),
+ )
};
+ let inlay_id = next_inlay.id;
log::info!(
- "creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}",
- inlay_id,
- position,
- bias,
- text
+ "creating inlay {inlay_id:?} at buffer offset {position} with bias {bias:?} and text {text:?}"
);
-
- to_insert.push(Inlay {
- id: inlay_id,
- position: snapshot.buffer.anchor_at(position, bias),
- text: text.into(),
- });
+ to_insert.push(next_inlay);
} else {
to_remove.push(
self.inlays
@@ -1183,11 +1220,11 @@ mod tests {
let (inlay_snapshot, _) = inlay_map.splice(
&[],
- vec![Inlay {
- id: InlayId::Hint(post_inc(&mut next_inlay_id)),
- position: buffer.read(cx).snapshot(cx).anchor_after(3),
- text: "|123|".into(),
- }],
+ vec![Inlay::mock_hint(
+ post_inc(&mut next_inlay_id),
+ buffer.read(cx).snapshot(cx).anchor_after(3),
+ "|123|",
+ )],
);
assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
assert_eq!(
@@ -1260,16 +1297,16 @@ mod tests {
let (inlay_snapshot, _) = inlay_map.splice(
&[],
vec![
- Inlay {
- id: InlayId::Hint(post_inc(&mut next_inlay_id)),
- position: buffer.read(cx).snapshot(cx).anchor_before(3),
- text: "|123|".into(),
- },
- Inlay {
- id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
- position: buffer.read(cx).snapshot(cx).anchor_after(3),
- text: "|456|".into(),
- },
+ Inlay::mock_hint(
+ post_inc(&mut next_inlay_id),
+ buffer.read(cx).snapshot(cx).anchor_before(3),
+ "|123|",
+ ),
+ Inlay::inline_completion(
+ post_inc(&mut next_inlay_id),
+ buffer.read(cx).snapshot(cx).anchor_after(3),
+ "|456|",
+ ),
],
);
assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
@@ -1475,21 +1512,21 @@ mod tests {
let (inlay_snapshot, _) = inlay_map.splice(
&[],
vec![
- Inlay {
- id: InlayId::Hint(post_inc(&mut next_inlay_id)),
- position: buffer.read(cx).snapshot(cx).anchor_before(0),
- text: "|123|\n".into(),
- },
- Inlay {
- id: InlayId::Hint(post_inc(&mut next_inlay_id)),
- position: buffer.read(cx).snapshot(cx).anchor_before(4),
- text: "|456|".into(),
- },
- Inlay {
- id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
- position: buffer.read(cx).snapshot(cx).anchor_before(7),
- text: "\n|567|\n".into(),
- },
+ Inlay::mock_hint(
+ post_inc(&mut next_inlay_id),
+ buffer.read(cx).snapshot(cx).anchor_before(0),
+ "|123|\n",
+ ),
+ Inlay::mock_hint(
+ post_inc(&mut next_inlay_id),
+ buffer.read(cx).snapshot(cx).anchor_before(4),
+ "|456|",
+ ),
+ Inlay::inline_completion(
+ post_inc(&mut next_inlay_id),
+ buffer.read(cx).snapshot(cx).anchor_before(7),
+ "\n|567|\n",
+ ),
],
);
assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
@@ -29,6 +29,7 @@ mod inlay_hint_cache;
pub mod items;
mod jsx_tag_auto_close;
mod linked_editing_ranges;
+mod lsp_colors;
mod lsp_ext;
mod mouse_context_menu;
pub mod movement;
@@ -63,8 +64,8 @@ use dap::TelemetrySpawnLocation;
use display_map::*;
pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder};
pub use editor_settings::{
- CurrentLineHighlight, EditorSettings, HideMouseMode, ScrollBeyondLastLine, ScrollbarAxes,
- SearchSettings, ShowScrollbar,
+ CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode,
+ ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowScrollbar,
};
use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings};
pub use editor_settings_controls::*;
@@ -79,6 +80,7 @@ use futures::{
stream::FuturesUnordered,
};
use fuzzy::{StringMatch, StringMatchCandidate};
+use lsp_colors::LspColorData;
use ::git::blame::BlameEntry;
use ::git::{Restore, blame::ParsedCommitMessage};
@@ -109,10 +111,9 @@ pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
use language::{
AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
- CursorShape, DiagnosticEntry, DiagnosticSourceKind, DiffOptions, DocumentationConfig,
- EditPredictionsMode, EditPreview, HighlightedText, IndentKind, IndentSize, Language,
- OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
- WordsQuery,
+ CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode,
+ EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point,
+ Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
language_settings::{
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
@@ -125,7 +126,7 @@ use markdown::Markdown;
use mouse_context_menu::MouseContextMenu;
use persistence::DB;
use project::{
- BreakpointWithPosition, CompletionResponse, LspPullDiagnostics, ProjectPath, PulledDiagnostics,
+ BreakpointWithPosition, CompletionResponse, ProjectPath,
debugger::{
breakpoint_store::{
BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
@@ -274,16 +275,19 @@ impl InlineValueCache {
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InlayId {
InlineCompletion(usize),
- Hint(usize),
DebuggerValue(usize),
+ // LSP
+ Hint(usize),
+ Color(usize),
}
impl InlayId {
fn id(&self) -> usize {
match self {
Self::InlineCompletion(id) => *id,
- Self::Hint(id) => *id,
Self::DebuggerValue(id) => *id,
+ Self::Hint(id) => *id,
+ Self::Color(id) => *id,
}
}
}
@@ -1134,6 +1138,8 @@ pub struct Editor {
inline_value_cache: InlineValueCache,
selection_drag_state: SelectionDragState,
drag_and_drop_selection_enabled: bool,
+ next_color_inlay_id: usize,
+ colors: Option<LspColorData>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
@@ -1795,13 +1801,13 @@ impl Editor {
editor
.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
}
- project::Event::LanguageServerAdded(..)
- | project::Event::LanguageServerRemoved(..) => {
+ project::Event::LanguageServerAdded(server_id, ..)
+ | project::Event::LanguageServerRemoved(server_id) => {
if editor.tasks_update_task.is_none() {
editor.tasks_update_task =
Some(editor.refresh_runnables(window, cx));
}
- editor.pull_diagnostics(None, window, cx);
+ editor.update_lsp_data(false, Some(*server_id), None, window, cx);
}
project::Event::SnippetEdit(id, snippet_edits) => {
if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
@@ -2070,6 +2076,8 @@ impl Editor {
],
tasks_update_task: None,
pull_diagnostics_task: Task::ready(()),
+ colors: None,
+ next_color_inlay_id: 0,
linked_edit_ranges: Default::default(),
in_project_search: false,
previous_search_ranges: None,
@@ -2211,7 +2219,8 @@ impl Editor {
editor.minimap =
editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx);
- editor.pull_diagnostics(None, window, cx);
+ editor.colors = Some(LspColorData::new(cx));
+ editor.update_lsp_data(false, None, None, window, cx);
}
editor.report_editor_event("Editor Opened", None, cx);
@@ -4899,6 +4908,15 @@ impl Editor {
.collect()
}
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn all_inlays(&self, cx: &App) -> Vec<Inlay> {
+ self.display_map
+ .read(cx)
+ .current_inlays()
+ .cloned()
+ .collect()
+ }
+
fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context<Self>) {
if self.semantics_provider.is_none() || !self.mode.is_full() {
return;
@@ -16241,8 +16259,14 @@ impl Editor {
let Ok(mut pull_diagnostics_tasks) = cx.update(|_, cx| {
buffers
.into_iter()
- .flat_map(|buffer| {
- Some(project.upgrade()?.pull_diagnostics_for_buffer(buffer, cx))
+ .filter_map(|buffer| {
+ project
+ .update(cx, |project, cx| {
+ project.lsp_store().update(cx, |lsp_store, cx| {
+ lsp_store.pull_diagnostics_for_buffer(buffer, cx)
+ })
+ })
+ .ok()
})
.collect::<FuturesUnordered<_>>()
}) else {
@@ -19066,7 +19090,7 @@ impl Editor {
.into_iter()
.flatten()
.for_each(|hint| {
- let inlay = Inlay::debugger_hint(
+ let inlay = Inlay::debugger(
post_inc(&mut editor.next_inlay_id),
Anchor::in_buffer(excerpt_id, buffer_id, hint.position),
hint.text(),
@@ -19117,17 +19141,15 @@ impl Editor {
.register_buffer_with_language_servers(&edited_buffer, cx)
});
});
- if edited_buffer.read(cx).file().is_some() {
- self.pull_diagnostics(
- Some(edited_buffer.read(cx).remote_id()),
- window,
- cx,
- );
- }
}
}
cx.emit(EditorEvent::BufferEdited);
cx.emit(SearchEvent::MatchesInvalidated);
+
+ if let Some(buffer) = edited_buffer {
+ self.update_lsp_data(true, None, Some(buffer.read(cx).remote_id()), window, cx);
+ }
+
if *singleton_buffer_edited {
if let Some(buffer) = edited_buffer {
if buffer.read(cx).file().is_none() {
@@ -19190,6 +19212,7 @@ impl Editor {
.detach();
}
}
+ self.update_lsp_data(false, None, Some(buffer_id), window, cx);
cx.emit(EditorEvent::ExcerptsAdded {
buffer: buffer.clone(),
predecessor: *predecessor,
@@ -19209,7 +19232,7 @@ impl Editor {
cx.emit(EditorEvent::ExcerptsRemoved {
ids: ids.clone(),
removed_buffer_ids: removed_buffer_ids.clone(),
- })
+ });
}
multi_buffer::Event::ExcerptsEdited {
excerpt_ids,
@@ -19220,7 +19243,7 @@ impl Editor {
});
cx.emit(EditorEvent::ExcerptsEdited {
ids: excerpt_ids.clone(),
- })
+ });
}
multi_buffer::Event::ExcerptsExpanded { ids } => {
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
@@ -19366,6 +19389,15 @@ impl Editor {
}
}
+ if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| {
+ colors.render_mode_updated(EditorSettings::get_global(cx).lsp_document_colors)
+ }) {
+ if !inlay_splice.to_insert.is_empty() || !inlay_splice.to_remove.is_empty() {
+ self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx);
+ }
+ self.refresh_colors(true, None, None, window, cx);
+ }
+
cx.notify();
}
@@ -20251,6 +20283,18 @@ impl Editor {
self.read_scroll_position_from_db(item_id, workspace_id, window, cx);
}
+
+ fn update_lsp_data(
+ &mut self,
+ update_on_edit: bool,
+ for_server_id: Option<LanguageServerId>,
+ for_buffer: Option<BufferId>,
+ window: &mut Window,
+ cx: &mut Context<'_, Self>,
+ ) {
+ self.pull_diagnostics(for_buffer, window, cx);
+ self.refresh_colors(update_on_edit, for_server_id, for_buffer, window, cx);
+ }
}
fn vim_enabled(cx: &App) -> bool {
@@ -20937,12 +20981,6 @@ pub trait SemanticsProvider {
new_name: String,
cx: &mut App,
) -> Option<Task<Result<ProjectTransaction>>>;
-
- fn pull_diagnostics_for_buffer(
- &self,
- buffer: Entity<Buffer>,
- cx: &mut App,
- ) -> Task<anyhow::Result<()>>;
}
pub trait CompletionProvider {
@@ -21460,85 +21498,6 @@ impl SemanticsProvider for Entity<Project> {
project.perform_rename(buffer.clone(), position, new_name, cx)
}))
}
-
- fn pull_diagnostics_for_buffer(
- &self,
- buffer: Entity<Buffer>,
- cx: &mut App,
- ) -> Task<anyhow::Result<()>> {
- let diagnostics = self.update(cx, |project, cx| {
- project
- .lsp_store()
- .update(cx, |lsp_store, cx| lsp_store.pull_diagnostics(buffer, cx))
- });
- let project = self.clone();
- cx.spawn(async move |cx| {
- let diagnostics = diagnostics.await.context("pulling diagnostics")?;
- project.update(cx, |project, cx| {
- project.lsp_store().update(cx, |lsp_store, cx| {
- for diagnostics_set in diagnostics {
- let LspPullDiagnostics::Response {
- server_id,
- uri,
- diagnostics,
- } = diagnostics_set
- else {
- continue;
- };
-
- let adapter = lsp_store.language_server_adapter_for_id(server_id);
- let disk_based_sources = adapter
- .as_ref()
- .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice())
- .unwrap_or(&[]);
- match diagnostics {
- PulledDiagnostics::Unchanged { result_id } => {
- lsp_store
- .merge_diagnostics(
- server_id,
- lsp::PublishDiagnosticsParams {
- uri: uri.clone(),
- diagnostics: Vec::new(),
- version: None,
- },
- Some(result_id),
- DiagnosticSourceKind::Pulled,
- disk_based_sources,
- |_, _| true,
- cx,
- )
- .log_err();
- }
- PulledDiagnostics::Changed {
- diagnostics,
- result_id,
- } => {
- lsp_store
- .merge_diagnostics(
- server_id,
- lsp::PublishDiagnosticsParams {
- uri: uri.clone(),
- diagnostics,
- version: None,
- },
- result_id,
- DiagnosticSourceKind::Pulled,
- disk_based_sources,
- |old_diagnostic, _| match old_diagnostic.source_kind {
- DiagnosticSourceKind::Pulled => false,
- DiagnosticSourceKind::Other
- | DiagnosticSourceKind::Pushed => true,
- },
- cx,
- )
- .log_err();
- }
- }
- }
- })
- })
- })
- }
}
fn inlay_hint_settings(
@@ -50,6 +50,22 @@ pub struct EditorSettings {
pub diagnostics_max_severity: Option<DiagnosticSeverity>,
pub inline_code_actions: bool,
pub drag_and_drop_selection: bool,
+ pub lsp_document_colors: DocumentColorsRenderMode,
+}
+
+/// How to render LSP `textDocument/documentColor` colors in the editor.
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum DocumentColorsRenderMode {
+ /// Do not query and render document colors.
+ None,
+ /// Render document colors as inlay hints near the color text.
+ #[default]
+ Inlay,
+ /// Draw a border around the color text.
+ Border,
+ /// Draw a background behind the color text.
+ Background,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -521,6 +537,11 @@ pub struct EditorSettingsContent {
///
/// Default: true
pub drag_and_drop_selection: Option<bool>,
+
+ /// How to render LSP `textDocument/documentColor` colors in the editor.
+ ///
+ /// Default: [`DocumentColorsRenderMode::Inlay`]
+ pub lsp_document_colors: Option<DocumentColorsRenderMode>,
}
// Toolbar related settings
@@ -22,8 +22,8 @@ use indoc::indoc;
use language::{
BracketPairConfig,
Capability::ReadWrite,
- FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
- Override, Point,
+ DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
+ LanguageName, Override, Point,
language_settings::{
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
LanguageSettingsContent, LspInsertMode, PrettierSettings,
@@ -16,8 +16,9 @@ use crate::{
ToDisplayPoint,
},
editor_settings::{
- CurrentLineHighlight, DoubleClickInMultibuffer, MinimapThumb, MinimapThumbBorder,
- ScrollBeyondLastLine, ScrollbarAxes, ScrollbarDiagnostics, ShowMinimap, ShowScrollbar,
+ CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, MinimapThumb,
+ MinimapThumbBorder, ScrollBeyondLastLine, ScrollbarAxes, ScrollbarDiagnostics, ShowMinimap,
+ ShowScrollbar,
},
git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
hover_popover::{
@@ -5676,6 +5677,7 @@ impl EditorElement {
self.paint_lines_background(layout, window, cx);
let invisible_display_ranges = self.paint_highlights(layout, window);
+ self.paint_document_colors(layout, window);
self.paint_lines(&invisible_display_ranges, layout, window, cx);
self.paint_redactions(layout, window);
self.paint_cursors(layout, window, cx);
@@ -5703,6 +5705,7 @@ impl EditorElement {
for (range, color) in &layout.highlighted_ranges {
self.paint_highlighted_range(
range.clone(),
+ true,
*color,
Pixels::ZERO,
line_end_overshoot,
@@ -5717,6 +5720,7 @@ impl EditorElement {
for selection in selections.iter() {
self.paint_highlighted_range(
selection.range.clone(),
+ true,
player_color.selection,
corner_radius,
corner_radius * 2.,
@@ -5792,6 +5796,7 @@ impl EditorElement {
for range in layout.redacted_ranges.iter() {
self.paint_highlighted_range(
range.clone(),
+ true,
redaction_color.into(),
Pixels::ZERO,
line_end_overshoot,
@@ -5802,6 +5807,48 @@ impl EditorElement {
});
}
+ fn paint_document_colors(&self, layout: &mut EditorLayout, window: &mut Window) {
+ let Some((colors_render_mode, image_colors)) = &layout.document_colors else {
+ return;
+ };
+ if image_colors.is_empty()
+ || colors_render_mode == &DocumentColorsRenderMode::None
+ || colors_render_mode == &DocumentColorsRenderMode::Inlay
+ {
+ return;
+ }
+
+ let line_end_overshoot = layout.line_end_overshoot();
+
+ for (range, color) in image_colors {
+ match colors_render_mode {
+ DocumentColorsRenderMode::Inlay | DocumentColorsRenderMode::None => return,
+ DocumentColorsRenderMode::Background => {
+ self.paint_highlighted_range(
+ range.clone(),
+ true,
+ *color,
+ Pixels::ZERO,
+ line_end_overshoot,
+ layout,
+ window,
+ );
+ }
+ DocumentColorsRenderMode::Border => {
+ self.paint_highlighted_range(
+ range.clone(),
+ false,
+ *color,
+ Pixels::ZERO,
+ line_end_overshoot,
+ layout,
+ window,
+ );
+ }
+ }
+ }
+ }
+
fn paint_cursors(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
for cursor in &mut layout.visible_cursors {
cursor.paint(layout.content_origin, window, cx);
@@ -6240,6 +6287,7 @@ impl EditorElement {
fn paint_highlighted_range(
&self,
range: Range<DisplayPoint>,
+ fill: bool,
color: Hsla,
corner_radius: Pixels,
line_end_overshoot: Pixels,
@@ -6290,7 +6338,7 @@ impl EditorElement {
.collect(),
};
- highlighted_range.paint(layout.position_map.text_hitbox.bounds, window);
+ highlighted_range.paint(fill, layout.position_map.text_hitbox.bounds, window);
}
}
@@ -8061,6 +8109,12 @@ impl Element for EditorElement {
cx,
);
+ let document_colors = self
+ .editor
+ .read(cx)
+ .colors
+ .as_ref()
+ .map(|colors| colors.editor_display_highlights(&snapshot));
let redacted_ranges = self.editor.read(cx).redacted_ranges(
start_anchor..end_anchor,
&snapshot.display_snapshot,
@@ -8808,6 +8862,7 @@ impl Element for EditorElement {
highlighted_ranges,
highlighted_gutter_ranges,
redacted_ranges,
+ document_colors,
line_elements,
line_numbers,
blamed_display_rows,
@@ -9013,6 +9068,7 @@ pub struct EditorLayout {
tab_invisible: ShapedLine,
space_invisible: ShapedLine,
sticky_buffer_header: Option<AnyElement>,
+ document_colors: Option<(DocumentColorsRenderMode, Vec<(Range<DisplayPoint>, Hsla)>)>,
}
impl EditorLayout {
@@ -9735,17 +9791,18 @@ pub struct HighlightedRangeLine {
}
impl HighlightedRange {
- pub fn paint(&self, bounds: Bounds<Pixels>, window: &mut Window) {
+ pub fn paint(&self, fill: bool, bounds: Bounds<Pixels>, window: &mut Window) {
if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
- self.paint_lines(self.start_y, &self.lines[0..1], bounds, window);
+ self.paint_lines(self.start_y, &self.lines[0..1], fill, bounds, window);
self.paint_lines(
self.start_y + self.line_height,
&self.lines[1..],
+ fill,
bounds,
window,
);
} else {
- self.paint_lines(self.start_y, &self.lines, bounds, window);
+ self.paint_lines(self.start_y, &self.lines, fill, bounds, window);
}
}
@@ -9753,6 +9810,7 @@ impl HighlightedRange {
&self,
start_y: Pixels,
lines: &[HighlightedRangeLine],
+ fill: bool,
_bounds: Bounds<Pixels>,
window: &mut Window,
) {
@@ -9779,7 +9837,11 @@ impl HighlightedRange {
};
let top_curve_width = curve_width(first_line.start_x, first_line.end_x);
- let mut builder = gpui::PathBuilder::fill();
+ let mut builder = if fill {
+ gpui::PathBuilder::fill()
+ } else {
+ gpui::PathBuilder::stroke(px(1.))
+ };
builder.move_to(first_top_right - top_curve_width);
builder.curve_to(first_top_right + curve_height, first_top_right);
@@ -0,0 +1,358 @@
+use std::{cmp, ops::Range};
+
+use collections::HashMap;
+use futures::future::join_all;
+use gpui::{Hsla, Rgba};
+use language::point_from_lsp;
+use lsp::LanguageServerId;
+use multi_buffer::Anchor;
+use project::DocumentColor;
+use settings::Settings as _;
+use text::{Bias, BufferId, OffsetRangeExt as _};
+use ui::{App, Context, Window};
+use util::post_inc;
+
+use crate::{
+ DisplayPoint, Editor, EditorSettings, EditorSnapshot, InlayId, InlaySplice, RangeToAnchorExt,
+ display_map::Inlay, editor_settings::DocumentColorsRenderMode,
+};
+
+#[derive(Debug)]
+pub(super) struct LspColorData {
+ colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
+ inlay_colors: HashMap<InlayId, usize>,
+ render_mode: DocumentColorsRenderMode,
+}
+
+impl LspColorData {
+ pub fn new(cx: &App) -> Self {
+ Self {
+ colors: Vec::new(),
+ inlay_colors: HashMap::default(),
+ render_mode: EditorSettings::get_global(cx).lsp_document_colors,
+ }
+ }
+
+ pub fn render_mode_updated(
+ &mut self,
+ new_render_mode: DocumentColorsRenderMode,
+ ) -> Option<InlaySplice> {
+ if self.render_mode == new_render_mode {
+ return None;
+ }
+ self.render_mode = new_render_mode;
+ match new_render_mode {
+ DocumentColorsRenderMode::Inlay => Some(InlaySplice {
+ to_remove: Vec::new(),
+ to_insert: self
+ .colors
+ .iter()
+ .map(|(range, color, id)| {
+ Inlay::color(
+ id.id(),
+ range.start,
+ Rgba {
+ r: color.color.red,
+ g: color.color.green,
+ b: color.color.blue,
+ a: color.color.alpha,
+ },
+ )
+ })
+ .collect(),
+ }),
+ DocumentColorsRenderMode::None => {
+ self.colors.clear();
+ Some(InlaySplice {
+ to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
+ to_insert: Vec::new(),
+ })
+ }
+ DocumentColorsRenderMode::Border | DocumentColorsRenderMode::Background => {
+ Some(InlaySplice {
+ to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
+ to_insert: Vec::new(),
+ })
+ }
+ }
+ }
+
+ fn set_colors(&mut self, colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>) -> bool {
+ if self.colors == colors {
+ return false;
+ }
+
+ self.inlay_colors = colors
+ .iter()
+ .enumerate()
+ .map(|(i, (_, _, id))| (*id, i))
+ .collect();
+ self.colors = colors;
+ true
+ }
+
+ pub fn editor_display_highlights(
+ &self,
+ snapshot: &EditorSnapshot,
+ ) -> (DocumentColorsRenderMode, Vec<(Range<DisplayPoint>, Hsla)>) {
+ let render_mode = self.render_mode;
+ let highlights = if render_mode == DocumentColorsRenderMode::None
+ || render_mode == DocumentColorsRenderMode::Inlay
+ {
+ Vec::new()
+ } else {
+ self.colors
+ .iter()
+ .map(|(range, color, _)| {
+ let display_range = range.clone().to_display_points(snapshot);
+ let color = Hsla::from(Rgba {
+ r: color.color.red,
+ g: color.color.green,
+ b: color.color.blue,
+ a: color.color.alpha,
+ });
+ (display_range, color)
+ })
+ .collect()
+ };
+ (render_mode, highlights)
+ }
+}
+
+impl Editor {
+ pub(super) fn refresh_colors(
+ &mut self,
+ update_on_edit: bool,
+ for_server_id: Option<LanguageServerId>,
+ buffer_id: Option<BufferId>,
+ _: &Window,
+ cx: &mut Context<Self>,
+ ) {
+ if !self.mode().is_full() {
+ return;
+ }
+ let Some(project) = self.project.clone() else {
+ return;
+ };
+ if self
+ .colors
+ .as_ref()
+ .is_none_or(|colors| colors.render_mode == DocumentColorsRenderMode::None)
+ {
+ return;
+ }
+
+ let all_colors_task = project.read(cx).lsp_store().update(cx, |lsp_store, cx| {
+ self.buffer()
+ .update(cx, |multi_buffer, cx| {
+ multi_buffer
+ .all_buffers()
+ .into_iter()
+ .filter(|editor_buffer| {
+ buffer_id.is_none_or(|buffer_id| {
+ buffer_id == editor_buffer.read(cx).remote_id()
+ })
+ })
+ .collect::<Vec<_>>()
+ })
+ .into_iter()
+ .filter_map(|buffer| {
+ let buffer_id = buffer.read(cx).remote_id();
+ let colors_task =
+ lsp_store.document_colors(update_on_edit, for_server_id, buffer, cx)?;
+ Some(async move { (buffer_id, colors_task.await) })
+ })
+ .collect::<Vec<_>>()
+ });
+ cx.spawn(async move |editor, cx| {
+ let all_colors = join_all(all_colors_task).await;
+ let Ok((multi_buffer_snapshot, editor_excerpts)) = editor.update(cx, |editor, cx| {
+ let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
+ let editor_excerpts = multi_buffer_snapshot.excerpts().fold(
+ HashMap::default(),
+ |mut acc, (excerpt_id, buffer_snapshot, excerpt_range)| {
+ let excerpt_data = acc
+ .entry(buffer_snapshot.remote_id())
+ .or_insert_with(Vec::new);
+ let excerpt_point_range =
+ excerpt_range.context.to_point_utf16(&buffer_snapshot);
+ excerpt_data.push((
+ excerpt_id,
+ buffer_snapshot.clone(),
+ excerpt_point_range,
+ ));
+ acc
+ },
+ );
+ (multi_buffer_snapshot, editor_excerpts)
+ }) else {
+ return;
+ };
+
+ let mut new_editor_colors = Vec::<(Range<Anchor>, DocumentColor)>::new();
+ for (buffer_id, colors) in all_colors {
+ let Some(excerpts) = editor_excerpts.get(&buffer_id) else {
+ continue;
+ };
+ match colors {
+ Ok(colors) => {
+ for color in colors {
+ let color_start = point_from_lsp(color.lsp_range.start);
+ let color_end = point_from_lsp(color.lsp_range.end);
+
+ for (excerpt_id, buffer_snapshot, excerpt_range) in excerpts {
+ if !excerpt_range.contains(&color_start.0)
+ || !excerpt_range.contains(&color_end.0)
+ {
+ continue;
+ }
+ let Some(color_start_anchor) = multi_buffer_snapshot
+ .anchor_in_excerpt(
+ *excerpt_id,
+ buffer_snapshot.anchor_before(
+ buffer_snapshot
+ .clip_point_utf16(color_start, Bias::Left),
+ ),
+ )
+ else {
+ continue;
+ };
+ let Some(color_end_anchor) = multi_buffer_snapshot
+ .anchor_in_excerpt(
+ *excerpt_id,
+ buffer_snapshot.anchor_after(
+ buffer_snapshot
+ .clip_point_utf16(color_end, Bias::Right),
+ ),
+ )
+ else {
+ continue;
+ };
+
+ let (Ok(i) | Err(i)) =
+ new_editor_colors.binary_search_by(|(probe, _)| {
+ probe
+ .start
+ .cmp(&color_start_anchor, &multi_buffer_snapshot)
+ .then_with(|| {
+ probe
+ .end
+ .cmp(&color_end_anchor, &multi_buffer_snapshot)
+ })
+ });
+ new_editor_colors
+ .insert(i, (color_start_anchor..color_end_anchor, color));
+ break;
+ }
+ }
+ }
+ Err(e) => log::error!("Failed to retrieve document colors: {e}"),
+ }
+ }
+
+ editor
+ .update(cx, |editor, cx| {
+ let mut colors_splice = InlaySplice::default();
+ let mut new_color_inlays = Vec::with_capacity(new_editor_colors.len());
+ let Some(colors) = &mut editor.colors else {
+ return;
+ };
+ let mut existing_colors = colors.colors.iter().peekable();
+ for (new_range, new_color) in new_editor_colors {
+ let rgba_color = Rgba {
+ r: new_color.color.red,
+ g: new_color.color.green,
+ b: new_color.color.blue,
+ a: new_color.color.alpha,
+ };
+
+ loop {
+ match existing_colors.peek() {
+ Some((existing_range, existing_color, existing_inlay_id)) => {
+ match existing_range
+ .start
+ .cmp(&new_range.start, &multi_buffer_snapshot)
+ .then_with(|| {
+ existing_range
+ .end
+ .cmp(&new_range.end, &multi_buffer_snapshot)
+ }) {
+ cmp::Ordering::Less => {
+ colors_splice.to_remove.push(*existing_inlay_id);
+ existing_colors.next();
+ continue;
+ }
+ cmp::Ordering::Equal => {
+ if existing_color == &new_color {
+ new_color_inlays.push((
+ new_range,
+ new_color,
+ *existing_inlay_id,
+ ));
+ } else {
+ colors_splice.to_remove.push(*existing_inlay_id);
+
+ let inlay = Inlay::color(
+ post_inc(&mut editor.next_color_inlay_id),
+ new_range.start,
+ rgba_color,
+ );
+ let inlay_id = inlay.id;
+ colors_splice.to_insert.push(inlay);
+ new_color_inlays
+ .push((new_range, new_color, inlay_id));
+ }
+ existing_colors.next();
+ break;
+ }
+ cmp::Ordering::Greater => {
+ let inlay = Inlay::color(
+ post_inc(&mut editor.next_color_inlay_id),
+ new_range.start,
+ rgba_color,
+ );
+ let inlay_id = inlay.id;
+ colors_splice.to_insert.push(inlay);
+ new_color_inlays.push((new_range, new_color, inlay_id));
+ break;
+ }
+ }
+ }
+ None => {
+ let inlay = Inlay::color(
+ post_inc(&mut editor.next_color_inlay_id),
+ new_range.start,
+ rgba_color,
+ );
+ let inlay_id = inlay.id;
+ colors_splice.to_insert.push(inlay);
+ new_color_inlays.push((new_range, new_color, inlay_id));
+ break;
+ }
+ }
+ }
+ }
+ if existing_colors.peek().is_some() {
+ colors_splice
+ .to_remove
+ .extend(existing_colors.map(|(_, _, id)| *id));
+ }
+
+ let mut updated = colors.set_colors(new_color_inlays);
+ if colors.render_mode == DocumentColorsRenderMode::Inlay
+ && (!colors_splice.to_insert.is_empty()
+ || !colors_splice.to_remove.is_empty())
+ {
+ editor.splice_inlays(&colors_splice.to_remove, colors_splice.to_insert, cx);
+ updated = true;
+ }
+
+ if updated {
+ cx.notify();
+ }
+ })
+ .ok();
+ })
+ .detach();
+ }
+}
@@ -789,7 +789,7 @@ pub fn split_display_range_by_lines(
mod tests {
use super::*;
use crate::{
- Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer,
+ Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, MultiBuffer,
display_map::Inlay,
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
};
@@ -939,26 +939,26 @@ mod tests {
let inlays = (0..buffer_snapshot.len())
.flat_map(|offset| {
[
- Inlay {
- id: InlayId::InlineCompletion(post_inc(&mut id)),
- position: buffer_snapshot.anchor_at(offset, Bias::Left),
- text: "test".into(),
- },
- Inlay {
- id: InlayId::InlineCompletion(post_inc(&mut id)),
- position: buffer_snapshot.anchor_at(offset, Bias::Right),
- text: "test".into(),
- },
- Inlay {
- id: InlayId::Hint(post_inc(&mut id)),
- position: buffer_snapshot.anchor_at(offset, Bias::Left),
- text: "test".into(),
- },
- Inlay {
- id: InlayId::Hint(post_inc(&mut id)),
- position: buffer_snapshot.anchor_at(offset, Bias::Right),
- text: "test".into(),
- },
+ Inlay::inline_completion(
+ post_inc(&mut id),
+ buffer_snapshot.anchor_at(offset, Bias::Left),
+ "test",
+ ),
+ Inlay::inline_completion(
+ post_inc(&mut id),
+ buffer_snapshot.anchor_at(offset, Bias::Right),
+ "test",
+ ),
+ Inlay::mock_hint(
+ post_inc(&mut id),
+ buffer_snapshot.anchor_at(offset, Bias::Left),
+ "test",
+ ),
+ Inlay::mock_hint(
+ post_inc(&mut id),
+ buffer_snapshot.anchor_at(offset, Bias::Right),
+ "test",
+ ),
]
})
.collect();
@@ -522,12 +522,4 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
) -> Option<Task<anyhow::Result<project::ProjectTransaction>>> {
None
}
-
- fn pull_diagnostics_for_buffer(
- &self,
- _: Entity<Buffer>,
- _: &mut App,
- ) -> Task<anyhow::Result<()>> {
- Task::ready(Ok(()))
- }
}
@@ -11,6 +11,8 @@ use text::*;
pub use proto::{BufferState, File, Operation};
+use super::{point_from_lsp, point_to_lsp};
+
/// Deserializes a `[text::LineEnding]` from the RPC representation.
pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
match message {
@@ -582,3 +584,33 @@ pub fn serialize_version(version: &clock::Global) -> Vec<proto::VectorClockEntry
})
.collect()
}
+
+pub fn serialize_lsp_edit(edit: lsp::TextEdit) -> proto::TextEdit {
+ let start = point_from_lsp(edit.range.start).0;
+ let end = point_from_lsp(edit.range.end).0;
+ proto::TextEdit {
+ new_text: edit.new_text,
+ lsp_range_start: Some(proto::PointUtf16 {
+ row: start.row,
+ column: start.column,
+ }),
+ lsp_range_end: Some(proto::PointUtf16 {
+ row: end.row,
+ column: end.column,
+ }),
+ }
+}
+
+pub fn deserialize_lsp_edit(edit: proto::TextEdit) -> Option<lsp::TextEdit> {
+ let start = edit.lsp_range_start?;
+ let start = PointUtf16::new(start.row, start.column);
+ let end = edit.lsp_range_end?;
+ let end = PointUtf16::new(end.row, end.column);
+ Some(lsp::TextEdit {
+ range: lsp::Range {
+ start: point_to_lsp(start),
+ end: point_to_lsp(end),
+ },
+ new_text: edit.new_text,
+ })
+}
@@ -804,6 +804,9 @@ impl LanguageServer {
related_document_support: Some(true),
})
.filter(|_| pull_diagnostics),
+ color_provider: Some(DocumentColorClientCapabilities {
+ dynamic_registration: Some(false),
+ }),
..TextDocumentClientCapabilities::default()
}),
experimental: Some(json!({
@@ -1,11 +1,11 @@
mod signature_help;
use crate::{
- CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentHighlight,
- DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
- InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
- LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse, ProjectTransaction,
- PulledDiagnostics, ResolveState,
+ CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentColor,
+ DocumentHighlight, DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint,
+ InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location,
+ LocationLink, LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse,
+ ProjectTransaction, PulledDiagnostics, ResolveState,
lsp_store::{LocalLspStore, LspStore},
};
use anyhow::{Context as _, Result};
@@ -244,6 +244,9 @@ pub(crate) struct InlayHints {
#[derive(Debug, Copy, Clone)]
pub(crate) struct GetCodeLens;
+#[derive(Debug, Copy, Clone)]
+pub(crate) struct GetDocumentColor;
+
impl GetCodeLens {
pub(crate) fn can_resolve_lens(capabilities: &ServerCapabilities) -> bool {
capabilities
@@ -4143,6 +4146,144 @@ impl LspCommand for GetDocumentDiagnostics {
}
}
+#[async_trait(?Send)]
+impl LspCommand for GetDocumentColor {
+ type Response = Vec<DocumentColor>;
+ type LspRequest = lsp::request::DocumentColor;
+ type ProtoRequest = proto::GetDocumentColor;
+
+ fn display_name(&self) -> &str {
+ "Document color"
+ }
+
+ fn check_capabilities(&self, server_capabilities: AdapterServerCapabilities) -> bool {
+ server_capabilities
+ .server_capabilities
+ .color_provider
+ .is_some()
+ }
+
+ fn to_lsp(
+ &self,
+ path: &Path,
+ _: &Buffer,
+ _: &Arc<LanguageServer>,
+ _: &App,
+ ) -> Result<lsp::DocumentColorParams> {
+ Ok(lsp::DocumentColorParams {
+ text_document: make_text_document_identifier(path)?,
+ work_done_progress_params: Default::default(),
+ partial_result_params: Default::default(),
+ })
+ }
+
+ async fn response_from_lsp(
+ self,
+ message: Vec<lsp::ColorInformation>,
+ _: Entity<LspStore>,
+ _: Entity<Buffer>,
+ _: LanguageServerId,
+ _: AsyncApp,
+ ) -> Result<Self::Response> {
+ Ok(message
+ .into_iter()
+ .map(|color| DocumentColor {
+ lsp_range: color.range,
+ color: color.color,
+ resolved: false,
+ color_presentations: Vec::new(),
+ })
+ .collect())
+ }
+
+ fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest {
+ proto::GetDocumentColor {
+ project_id,
+ buffer_id: buffer.remote_id().to_proto(),
+ version: serialize_version(&buffer.version()),
+ }
+ }
+
+ async fn from_proto(
+ _: Self::ProtoRequest,
+ _: Entity<LspStore>,
+ _: Entity<Buffer>,
+ _: AsyncApp,
+ ) -> Result<Self> {
+ Ok(Self {})
+ }
+
+ fn response_to_proto(
+ response: Self::Response,
+ _: &mut LspStore,
+ _: PeerId,
+ buffer_version: &clock::Global,
+ _: &mut App,
+ ) -> proto::GetDocumentColorResponse {
+ proto::GetDocumentColorResponse {
+ colors: response
+ .into_iter()
+ .map(|color| {
+ let start = point_from_lsp(color.lsp_range.start).0;
+ let end = point_from_lsp(color.lsp_range.end).0;
+ proto::ColorInformation {
+ red: color.color.red,
+ green: color.color.green,
+ blue: color.color.blue,
+ alpha: color.color.alpha,
+ lsp_range_start: Some(proto::PointUtf16 {
+ row: start.row,
+ column: start.column,
+ }),
+ lsp_range_end: Some(proto::PointUtf16 {
+ row: end.row,
+ column: end.column,
+ }),
+ }
+ })
+ .collect(),
+ version: serialize_version(buffer_version),
+ }
+ }
+
+ async fn response_from_proto(
+ self,
+ message: proto::GetDocumentColorResponse,
+ _: Entity<LspStore>,
+ _: Entity<Buffer>,
+ _: AsyncApp,
+ ) -> Result<Self::Response> {
+ Ok(message
+ .colors
+ .into_iter()
+ .filter_map(|color| {
+ let start = color.lsp_range_start?;
+ let start = PointUtf16::new(start.row, start.column);
+ let end = color.lsp_range_end?;
+ let end = PointUtf16::new(end.row, end.column);
+ Some(DocumentColor {
+ resolved: false,
+ color_presentations: Vec::new(),
+ lsp_range: lsp::Range {
+ start: point_to_lsp(start),
+ end: point_to_lsp(end),
+ },
+ color: lsp::Color {
+ red: color.red,
+ green: color.green,
+ blue: color.blue,
+ alpha: color.alpha,
+ },
+ })
+ })
+ .collect())
+ }
+
+ fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result<BufferId> {
+ BufferId::new(message.buffer_id)
+ }
+}
+
fn process_related_documents(
diagnostics: &mut HashMap<lsp::Url, LspPullDiagnostics>,
server_id: LanguageServerId,
@@ -3,9 +3,9 @@ pub mod lsp_ext_command;
pub mod rust_analyzer_ext;
use crate::{
- CodeAction, Completion, CompletionResponse, CompletionSource, CoreCompletion, Hover, InlayHint,
- LspAction, LspPullDiagnostics, ProjectItem, ProjectPath, ProjectTransaction, PulledDiagnostics,
- ResolveState, Symbol, ToolchainStore,
+ CodeAction, ColorPresentation, Completion, CompletionResponse, CompletionSource,
+ CoreCompletion, DocumentColor, Hover, InlayHint, LspAction, LspPullDiagnostics, ProjectItem,
+ ProjectPath, ProjectTransaction, PulledDiagnostics, ResolveState, Symbol, ToolchainStore,
buffer_store::{BufferStore, BufferStoreEvent},
environment::ProjectEnvironment,
lsp_command::{self, *},
@@ -24,6 +24,7 @@ use crate::{
use anyhow::{Context as _, Result, anyhow};
use async_trait::async_trait;
use client::{TypedEnvelope, proto};
+use clock::Global;
use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map};
use futures::{
AsyncWriteExt, Future, FutureExt, StreamExt,
@@ -48,7 +49,10 @@ use language::{
FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings,
},
point_to_lsp,
- proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
+ proto::{
+ deserialize_anchor, deserialize_lsp_edit, deserialize_version, serialize_anchor,
+ serialize_lsp_edit, serialize_version,
+ },
range_from_lsp, range_to_lsp,
};
use lsp::{
@@ -320,7 +324,7 @@ impl LocalLspStore {
if let Some(lsp_store) = this.upgrade() {
lsp_store
.update(cx, |lsp_store, cx| {
- lsp_store.remove_result_ids(server_id);
+ lsp_store.cleanup_lsp_data(server_id);
cx.emit(LspStoreEvent::LanguageServerRemoved(server_id))
})
.ok();
@@ -3481,6 +3485,22 @@ pub struct LspStore {
_maintain_buffer_languages: Task<()>,
diagnostic_summaries:
HashMap<WorktreeId, HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>>,
+ lsp_data: Option<LspData>,
+}
+
+type DocumentColorTask = Shared<Task<std::result::Result<Vec<DocumentColor>, Arc<anyhow::Error>>>>;
+
+#[derive(Debug)]
+struct LspData {
+ mtime: MTime,
+ buffer_lsp_data: HashMap<LanguageServerId, HashMap<PathBuf, BufferLspData>>,
+ colors_update: HashMap<PathBuf, DocumentColorTask>,
+ last_version_queried: HashMap<PathBuf, Global>,
+}
+
+#[derive(Debug, Default)]
+struct BufferLspData {
+ colors: Option<Vec<DocumentColor>>,
}
pub enum LspStoreEvent {
@@ -3553,6 +3573,7 @@ impl LspStore {
client.add_entity_request_handler(Self::handle_inlay_hints);
client.add_entity_request_handler(Self::handle_get_project_symbols);
client.add_entity_request_handler(Self::handle_resolve_inlay_hint);
+ client.add_entity_request_handler(Self::handle_get_color_presentation);
client.add_entity_request_handler(Self::handle_open_buffer_for_symbol);
client.add_entity_request_handler(Self::handle_refresh_inlay_hints);
client.add_entity_request_handler(Self::handle_refresh_code_lens);
@@ -3707,9 +3728,9 @@ impl LspStore {
languages: languages.clone(),
language_server_statuses: Default::default(),
nonce: StdRng::from_entropy().r#gen(),
- diagnostic_summaries: Default::default(),
+ diagnostic_summaries: HashMap::default(),
+ lsp_data: None,
active_entry: None,
-
_maintain_workspace_config,
_maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx),
}
@@ -3763,7 +3784,8 @@ impl LspStore {
languages: languages.clone(),
language_server_statuses: Default::default(),
nonce: StdRng::from_entropy().r#gen(),
- diagnostic_summaries: Default::default(),
+ diagnostic_summaries: HashMap::default(),
+ lsp_data: None,
active_entry: None,
toolchain_store,
_maintain_workspace_config,
@@ -3890,7 +3912,7 @@ impl LspStore {
cx: &mut Context<Self>,
) {
match event {
- language::BufferEvent::Edited { .. } => {
+ language::BufferEvent::Edited => {
self.on_buffer_edited(buffer, cx);
}
@@ -4835,6 +4857,105 @@ impl LspStore {
}
}
+ pub fn resolve_color_presentation(
+ &mut self,
+ mut color: DocumentColor,
+ buffer: Entity<Buffer>,
+ server_id: LanguageServerId,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<DocumentColor>> {
+ if color.resolved {
+ return Task::ready(Ok(color));
+ }
+
+ if let Some((upstream_client, project_id)) = self.upstream_client() {
+ let start = color.lsp_range.start;
+ let end = color.lsp_range.end;
+ let request = proto::GetColorPresentation {
+ project_id,
+ server_id: server_id.to_proto(),
+ buffer_id: buffer.read(cx).remote_id().into(),
+ color: Some(proto::ColorInformation {
+ red: color.color.red,
+ green: color.color.green,
+ blue: color.color.blue,
+ alpha: color.color.alpha,
+ lsp_range_start: Some(proto::PointUtf16 {
+ row: start.line,
+ column: start.character,
+ }),
+ lsp_range_end: Some(proto::PointUtf16 {
+ row: end.line,
+ column: end.character,
+ }),
+ }),
+ };
+ cx.background_spawn(async move {
+ let response = upstream_client
+ .request(request)
+ .await
+ .context("color presentation proto request")?;
+ color.resolved = true;
+ color.color_presentations = response
+ .presentations
+ .into_iter()
+ .map(|presentation| ColorPresentation {
+ label: presentation.label,
+ text_edit: presentation.text_edit.and_then(deserialize_lsp_edit),
+ additional_text_edits: presentation
+ .additional_text_edits
+ .into_iter()
+ .filter_map(deserialize_lsp_edit)
+ .collect(),
+ })
+ .collect();
+ Ok(color)
+ })
+ } else {
+ let path = match buffer
+ .update(cx, |buffer, cx| {
+ Some(crate::File::from_dyn(buffer.file())?.abs_path(cx))
+ })
+ .context("buffer with the missing path")
+ {
+ Ok(path) => path,
+ Err(e) => return Task::ready(Err(e)),
+ };
+ let Some(lang_server) = buffer.update(cx, |buffer, cx| {
+ self.language_server_for_local_buffer(buffer, server_id, cx)
+ .map(|(_, server)| server.clone())
+ }) else {
+ return Task::ready(Ok(color));
+ };
+ cx.background_spawn(async move {
+ let resolve_task = lang_server.request::<lsp::request::ColorPresentationRequest>(
+ lsp::ColorPresentationParams {
+ text_document: make_text_document_identifier(&path)?,
+ color: color.color,
+ range: color.lsp_range,
+ work_done_progress_params: Default::default(),
+ partial_result_params: Default::default(),
+ },
+ );
+ color.color_presentations = resolve_task
+ .await
+ .into_response()
+ .context("color presentation resolve LSP request")?
+ .into_iter()
+ .map(|presentation| ColorPresentation {
+ label: presentation.label,
+ text_edit: presentation.text_edit,
+ additional_text_edits: presentation
+ .additional_text_edits
+ .unwrap_or_default(),
+ })
+ .collect();
+ color.resolved = true;
+ Ok(color)
+ })
+ }
+ }
+
pub(crate) fn linked_edit(
&mut self,
buffer: &Entity<Buffer>,
@@ -5063,7 +5184,13 @@ impl LspStore {
},
cx,
);
- cx.spawn(async move |_, _| Ok(all_actions_task.await.into_iter().flatten().collect()))
+ cx.spawn(async move |_, _| {
+ Ok(all_actions_task
+ .await
+ .into_iter()
+ .flat_map(|(_, actions)| actions)
+ .collect())
+ })
}
}
@@ -5123,7 +5250,13 @@ impl LspStore {
} else {
let code_lens_task =
self.request_multiple_lsp_locally(buffer_handle, None::<usize>, GetCodeLens, cx);
- cx.spawn(async move |_, _| Ok(code_lens_task.await.into_iter().flatten().collect()))
+ cx.spawn(async move |_, _| {
+ Ok(code_lens_task
+ .await
+ .into_iter()
+ .flat_map(|(_, code_lens)| code_lens)
+ .collect())
+ })
}
}
@@ -5870,6 +6003,293 @@ impl LspStore {
}
}
+ pub fn pull_diagnostics_for_buffer(
+ &mut self,
+ buffer: Entity<Buffer>,
+ cx: &mut Context<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ let diagnostics = self.pull_diagnostics(buffer, cx);
+ cx.spawn(async move |lsp_store, cx| {
+ let diagnostics = diagnostics.await.context("pulling diagnostics")?;
+ lsp_store.update(cx, |lsp_store, cx| {
+ for diagnostics_set in diagnostics {
+ let LspPullDiagnostics::Response {
+ server_id,
+ uri,
+ diagnostics,
+ } = diagnostics_set
+ else {
+ continue;
+ };
+
+ let adapter = lsp_store.language_server_adapter_for_id(server_id);
+ let disk_based_sources = adapter
+ .as_ref()
+ .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice())
+ .unwrap_or(&[]);
+ match diagnostics {
+ PulledDiagnostics::Unchanged { result_id } => {
+ lsp_store
+ .merge_diagnostics(
+ server_id,
+ lsp::PublishDiagnosticsParams {
+ uri: uri.clone(),
+ diagnostics: Vec::new(),
+ version: None,
+ },
+ Some(result_id),
+ DiagnosticSourceKind::Pulled,
+ disk_based_sources,
+ |_, _| true,
+ cx,
+ )
+ .log_err();
+ }
+ PulledDiagnostics::Changed {
+ diagnostics,
+ result_id,
+ } => {
+ lsp_store
+ .merge_diagnostics(
+ server_id,
+ lsp::PublishDiagnosticsParams {
+ uri: uri.clone(),
+ diagnostics,
+ version: None,
+ },
+ result_id,
+ DiagnosticSourceKind::Pulled,
+ disk_based_sources,
+ |old_diagnostic, _| match old_diagnostic.source_kind {
+ DiagnosticSourceKind::Pulled => false,
+ DiagnosticSourceKind::Other
+ | DiagnosticSourceKind::Pushed => true,
+ },
+ cx,
+ )
+ .log_err();
+ }
+ }
+ }
+ })
+ })
+ }
+
+ pub fn document_colors(
+ &mut self,
+ update_on_edit: bool,
+ for_server_id: Option<LanguageServerId>,
+ buffer: Entity<Buffer>,
+ cx: &mut Context<Self>,
+ ) -> Option<DocumentColorTask> {
+ let buffer_mtime = buffer.read(cx).saved_mtime()?;
+ let abs_path = crate::File::from_dyn(buffer.read(cx).file())?.abs_path(cx);
+ let buffer_version = buffer.read(cx).version();
+ let ignore_existing_mtime = update_on_edit
+ && self.lsp_data.as_ref().is_none_or(|lsp_data| {
+ lsp_data.last_version_queried.get(&abs_path) != Some(&buffer_version)
+ });
+
+ let mut has_other_versions = false;
+ let mut received_colors_data = false;
+ let mut outdated_lsp_data = false;
+ let buffer_lsp_data = self
+ .lsp_data
+ .as_ref()
+ .into_iter()
+ .filter(|lsp_data| {
+ if ignore_existing_mtime {
+ return false;
+ }
+ has_other_versions |= lsp_data.mtime != buffer_mtime;
+ lsp_data.mtime == buffer_mtime
+ })
+ .flat_map(|lsp_data| lsp_data.buffer_lsp_data.values())
+ .filter_map(|buffer_data| buffer_data.get(&abs_path))
+ .filter_map(|buffer_data| {
+ let colors = buffer_data.colors.as_deref()?;
+ received_colors_data = true;
+ Some(colors)
+ })
+ .flatten()
+ .cloned()
+ .collect::<Vec<_>>();
+
+ if buffer_lsp_data.is_empty() || for_server_id.is_some() {
+ if received_colors_data && for_server_id.is_none() {
+ return None;
+ } else if has_other_versions && !ignore_existing_mtime {
+ return None;
+ }
+
+ if ignore_existing_mtime
+ || self.lsp_data.is_none()
+ || self
+ .lsp_data
+ .as_ref()
+ .is_some_and(|lsp_data| buffer_mtime != lsp_data.mtime)
+ {
+ self.lsp_data = Some(LspData {
+ mtime: buffer_mtime,
+ buffer_lsp_data: HashMap::default(),
+ colors_update: HashMap::default(),
+ last_version_queried: HashMap::default(),
+ });
+ outdated_lsp_data = true;
+ }
+
+ {
+ let lsp_data = self.lsp_data.as_mut()?;
+ match for_server_id {
+ Some(for_server_id) if !outdated_lsp_data => {
+ lsp_data.buffer_lsp_data.remove(&for_server_id);
+ }
+ None | Some(_) => {
+ let existing_task = lsp_data.colors_update.get(&abs_path).cloned();
+ if !outdated_lsp_data && existing_task.is_some() {
+ return existing_task;
+ }
+ for buffer_data in lsp_data.buffer_lsp_data.values_mut() {
+ if let Some(buffer_data) = buffer_data.get_mut(&abs_path) {
+ buffer_data.colors = None;
+ }
+ }
+ }
+ }
+ }
+
+ let task_abs_path = abs_path.clone();
+ let new_task = cx
+ .spawn(async move |lsp_store, cx| {
+ cx.background_executor().timer(Duration::from_millis(50)).await;
+ let fetched_colors = match lsp_store
+ .update(cx, |lsp_store, cx| {
+ lsp_store.fetch_document_colors(buffer, cx)
+ }) {
+ Ok(fetch_task) => fetch_task.await
+ .with_context(|| {
+ format!(
+ "Fetching document colors for buffer with path {task_abs_path:?}"
+ )
+ }),
+ Err(e) => return Err(Arc::new(e)),
+ };
+ let fetched_colors = match fetched_colors {
+ Ok(fetched_colors) => fetched_colors,
+ Err(e) => return Err(Arc::new(e)),
+ };
+
+ let lsp_colors = lsp_store.update(cx, |lsp_store, _| {
+ let lsp_data = lsp_store.lsp_data.as_mut().with_context(|| format!(
+ "Document lsp data got updated between fetch and update for path {task_abs_path:?}"
+ ))?;
+ let mut lsp_colors = Vec::new();
+ anyhow::ensure!(lsp_data.mtime == buffer_mtime, "Buffer lsp data got updated between fetch and update for path {task_abs_path:?}");
+ for (server_id, colors) in fetched_colors {
+ let colors_lsp_data = &mut lsp_data.buffer_lsp_data.entry(server_id).or_default().entry(task_abs_path.clone()).or_default().colors;
+ *colors_lsp_data = Some(colors.clone());
+ lsp_colors.extend(colors);
+ }
+ Ok(lsp_colors)
+ });
+
+ match lsp_colors {
+ Ok(Ok(lsp_colors)) => Ok(lsp_colors),
+ Ok(Err(e)) => Err(Arc::new(e)),
+ Err(e) => Err(Arc::new(e)),
+ }
+ })
+ .shared();
+ let lsp_data = self.lsp_data.as_mut()?;
+ lsp_data
+ .colors_update
+ .insert(abs_path.clone(), new_task.clone());
+ lsp_data
+ .last_version_queried
+ .insert(abs_path, buffer_version);
+ Some(new_task)
+ } else {
+ Some(Task::ready(Ok(buffer_lsp_data)).shared())
+ }
+ }
+
+ fn fetch_document_colors(
+ &mut self,
+ buffer: Entity<Buffer>,
+ cx: &mut Context<Self>,
+ ) -> Task<anyhow::Result<Vec<(LanguageServerId, Vec<DocumentColor>)>>> {
+ if let Some((client, project_id)) = self.upstream_client() {
+ let request_task = client.request(proto::MultiLspQuery {
+ project_id,
+ buffer_id: buffer.read(cx).remote_id().to_proto(),
+ version: serialize_version(&buffer.read(cx).version()),
+ strategy: Some(proto::multi_lsp_query::Strategy::All(
+ proto::AllLanguageServers {},
+ )),
+ request: Some(proto::multi_lsp_query::Request::GetDocumentColor(
+ GetDocumentColor {}.to_proto(project_id, buffer.read(cx)),
+ )),
+ });
+ cx.spawn(async move |project, cx| {
+ let Some(project) = project.upgrade() else {
+ return Ok(Vec::new());
+ };
+ let colors = join_all(
+ request_task
+ .await
+ .log_err()
+ .map(|response| response.responses)
+ .unwrap_or_default()
+ .into_iter()
+ .filter_map(|lsp_response| match lsp_response.response? {
+ proto::lsp_response::Response::GetDocumentColorResponse(response) => {
+ Some((
+ LanguageServerId::from_proto(lsp_response.server_id),
+ response,
+ ))
+ }
+ unexpected => {
+ debug_panic!("Unexpected response: {unexpected:?}");
+ None
+ }
+ })
+ .map(|(server_id, color_response)| {
+ let response = GetDocumentColor {}.response_from_proto(
+ color_response,
+ project.clone(),
+ buffer.clone(),
+ cx.clone(),
+ );
+ async move { (server_id, response.await.log_err().unwrap_or_default()) }
+ }),
+ )
+ .await
+ .into_iter()
+ .fold(HashMap::default(), |mut acc, (server_id, colors)| {
+ acc.entry(server_id).or_insert_with(Vec::new).extend(colors);
+ acc
+ })
+ .into_iter()
+ .collect();
+ Ok(colors)
+ })
+ } else {
+ let document_colors_task =
+ self.request_multiple_lsp_locally(&buffer, None::<usize>, GetDocumentColor, cx);
+ cx.spawn(async move |_, _| {
+ Ok(document_colors_task
+ .await
+ .into_iter()
+ .fold(HashMap::default(), |mut acc, (server_id, colors)| {
+ acc.entry(server_id).or_insert_with(Vec::new).extend(colors);
+ acc
+ })
+ .into_iter()
+ .collect())
+ })
+ }
+ }
+
pub fn signature_help<T: ToPointUtf16>(
&mut self,
buffer: &Entity<Buffer>,
@@ -5937,7 +6357,7 @@ impl LspStore {
all_actions_task
.await
.into_iter()
- .flatten()
+ .flat_map(|(_, actions)| actions)
.filter(|help| !help.label.is_empty())
.collect::<Vec<_>>()
})
@@ -6015,7 +6435,7 @@ impl LspStore {
all_actions_task
.await
.into_iter()
- .filter_map(|hover| remove_empty_hover_blocks(hover?))
+ .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?))
.collect::<Vec<Hover>>()
})
}
@@ -6948,7 +7368,7 @@ impl LspStore {
position: Option<P>,
request: R,
cx: &mut Context<Self>,
- ) -> Task<Vec<R::Response>>
+ ) -> Task<Vec<(LanguageServerId, R::Response)>>
where
P: ToOffset,
R: LspCommand + Clone,
@@ -6978,20 +7398,21 @@ impl LspStore {
let mut response_results = server_ids
.into_iter()
.map(|server_id| {
- self.request_lsp(
+ let task = self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Other(server_id),
request.clone(),
cx,
- )
+ );
+ async move { (server_id, task.await) }
})
.collect::<FuturesUnordered<_>>();
cx.spawn(async move |_, _| {
let mut responses = Vec::with_capacity(response_results.len());
- while let Some(response_result) = response_results.next().await {
+ while let Some((server_id, response_result)) = response_results.next().await {
if let Some(response) = response_result.log_err() {
- responses.push(response);
+ responses.push((server_id, response));
}
}
responses
@@ -7079,9 +7500,14 @@ impl LspStore {
}
}
match envelope.payload.request {
- Some(proto::multi_lsp_query::Request::GetHover(get_hover)) => {
+ Some(proto::multi_lsp_query::Request::GetHover(message)) => {
+ buffer
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_version(deserialize_version(&message.version))
+ })?
+ .await?;
let get_hover =
- GetHover::from_proto(get_hover, lsp_store.clone(), buffer.clone(), cx.clone())
+ GetHover::from_proto(message, lsp_store.clone(), buffer.clone(), cx.clone())
.await?;
let all_hovers = lsp_store
.update(&mut cx, |this, cx| {
@@ -7094,10 +7520,13 @@ impl LspStore {
})?
.await
.into_iter()
- .filter_map(|hover| remove_empty_hover_blocks(hover?));
+ .filter_map(|(server_id, hover)| {
+ Some((server_id, remove_empty_hover_blocks(hover?)?))
+ });
lsp_store.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
responses: all_hovers
- .map(|hover| proto::LspResponse {
+ .map(|(server_id, hover)| proto::LspResponse {
+ server_id: server_id.to_proto(),
response: Some(proto::lsp_response::Response::GetHoverResponse(
GetHover::response_to_proto(
Some(hover),
@@ -7111,9 +7540,14 @@ impl LspStore {
.collect(),
})
}
- Some(proto::multi_lsp_query::Request::GetCodeActions(get_code_actions)) => {
+ Some(proto::multi_lsp_query::Request::GetCodeActions(message)) => {
+ buffer
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_version(deserialize_version(&message.version))
+ })?
+ .await?;
let get_code_actions = GetCodeActions::from_proto(
- get_code_actions,
+ message,
lsp_store.clone(),
buffer.clone(),
cx.clone(),
@@ -7134,7 +7568,8 @@ impl LspStore {
lsp_store.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
responses: all_actions
- .map(|code_actions| proto::LspResponse {
+ .map(|(server_id, code_actions)| proto::LspResponse {
+ server_id: server_id.to_proto(),
response: Some(proto::lsp_response::Response::GetCodeActionsResponse(
GetCodeActions::response_to_proto(
code_actions,
@@ -7148,9 +7583,14 @@ impl LspStore {
.collect(),
})
}
- Some(proto::multi_lsp_query::Request::GetSignatureHelp(get_signature_help)) => {
+ Some(proto::multi_lsp_query::Request::GetSignatureHelp(message)) => {
+ buffer
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_version(deserialize_version(&message.version))
+ })?
+ .await?;
let get_signature_help = GetSignatureHelp::from_proto(
- get_signature_help,
+ message,
lsp_store.clone(),
buffer.clone(),
cx.clone(),
@@ -7171,7 +7611,8 @@ impl LspStore {
lsp_store.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
responses: all_signatures
- .map(|signature_help| proto::LspResponse {
+ .map(|(server_id, signature_help)| proto::LspResponse {
+ server_id: server_id.to_proto(),
response: Some(
proto::lsp_response::Response::GetSignatureHelpResponse(
GetSignatureHelp::response_to_proto(
@@ -7187,14 +7628,15 @@ impl LspStore {
.collect(),
})
}
- Some(proto::multi_lsp_query::Request::GetCodeLens(get_code_lens)) => {
- let get_code_lens = GetCodeLens::from_proto(
- get_code_lens,
- lsp_store.clone(),
- buffer.clone(),
- cx.clone(),
- )
- .await?;
+ Some(proto::multi_lsp_query::Request::GetCodeLens(message)) => {
+ buffer
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_version(deserialize_version(&message.version))
+ })?
+ .await?;
+ let get_code_lens =
+ GetCodeLens::from_proto(message, lsp_store.clone(), buffer.clone(), cx.clone())
+ .await?;
let code_lens_actions = lsp_store
.update(&mut cx, |project, cx| {
@@ -7210,7 +7652,8 @@ impl LspStore {
lsp_store.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
responses: code_lens_actions
- .map(|actions| proto::LspResponse {
+ .map(|(server_id, actions)| proto::LspResponse {
+ server_id: server_id.to_proto(),
response: Some(proto::lsp_response::Response::GetCodeLensResponse(
GetCodeLens::response_to_proto(
actions,
@@ -7242,29 +7685,30 @@ impl LspStore {
.into_iter()
.map(|server_id| {
let result_id = lsp_store.result_id(server_id, buffer_id, cx);
- lsp_store.request_lsp(
+ let task = lsp_store.request_lsp(
buffer.clone(),
LanguageServerToQuery::Other(server_id),
GetDocumentDiagnostics {
previous_result_id: result_id,
},
cx,
- )
+ );
+ async move { (server_id, task.await) }
})
.collect::<Vec<_>>()
})?;
let all_diagnostics_responses = join_all(pull_diagnostics).await;
let mut all_diagnostics = Vec::new();
- for response in all_diagnostics_responses {
- let response = response?;
- all_diagnostics.push(response);
+ for (server_id, response) in all_diagnostics_responses {
+ all_diagnostics.push((server_id, response?));
}
lsp_store.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
responses: all_diagnostics
.into_iter()
- .map(|lsp_diagnostic| proto::LspResponse {
+ .map(|(server_id, lsp_diagnostic)| proto::LspResponse {
+ server_id: server_id.to_proto(),
response: Some(
proto::lsp_response::Response::GetDocumentDiagnosticsResponse(
GetDocumentDiagnostics::response_to_proto(
@@ -7280,6 +7724,51 @@ impl LspStore {
.collect(),
})
}
+ Some(proto::multi_lsp_query::Request::GetDocumentColor(message)) => {
+ buffer
+ .update(&mut cx, |buffer, _| {
+ buffer.wait_for_version(deserialize_version(&message.version))
+ })?
+ .await?;
+ let get_document_color = GetDocumentColor::from_proto(
+ message,
+ lsp_store.clone(),
+ buffer.clone(),
+ cx.clone(),
+ )
+ .await?;
+
+ let all_colors = lsp_store
+ .update(&mut cx, |project, cx| {
+ project.request_multiple_lsp_locally(
+ &buffer,
+ None::<usize>,
+ get_document_color,
+ cx,
+ )
+ })?
+ .await
+ .into_iter();
+
+ lsp_store.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
+ responses: all_colors
+ .map(|(server_id, colors)| proto::LspResponse {
+ server_id: server_id.to_proto(),
+ response: Some(
+ proto::lsp_response::Response::GetDocumentColorResponse(
+ GetDocumentColor::response_to_proto(
+ colors,
+ project,
+ sender_id,
+ &buffer_version,
+ cx,
+ ),
+ ),
+ ),
+ })
+ .collect(),
+ })
+ }
None => anyhow::bail!("empty multi lsp query request"),
}
}
@@ -8263,6 +8752,70 @@ impl LspStore {
})
}
+ async fn handle_get_color_presentation(
+ lsp_store: Entity<Self>,
+ envelope: TypedEnvelope<proto::GetColorPresentation>,
+ mut cx: AsyncApp,
+ ) -> Result<proto::GetColorPresentationResponse> {
+ let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
+ let buffer = lsp_store.update(&mut cx, |lsp_store, cx| {
+ lsp_store.buffer_store.read(cx).get_existing(buffer_id)
+ })??;
+
+ let color = envelope
+ .payload
+ .color
+ .context("invalid color resolve request")?;
+ let start = color
+ .lsp_range_start
+ .context("invalid color resolve request")?;
+ let end = color
+ .lsp_range_end
+ .context("invalid color resolve request")?;
+
+ let color = DocumentColor {
+ lsp_range: lsp::Range {
+ start: point_to_lsp(PointUtf16::new(start.row, start.column)),
+ end: point_to_lsp(PointUtf16::new(end.row, end.column)),
+ },
+ color: lsp::Color {
+ red: color.red,
+ green: color.green,
+ blue: color.blue,
+ alpha: color.alpha,
+ },
+ resolved: false,
+ color_presentations: Vec::new(),
+ };
+ let resolved_color = lsp_store
+ .update(&mut cx, |lsp_store, cx| {
+ lsp_store.resolve_color_presentation(
+ color,
+ buffer.clone(),
+ LanguageServerId(envelope.payload.server_id as usize),
+ cx,
+ )
+ })?
+ .await
+ .context("resolving color presentation")?;
+
+ Ok(proto::GetColorPresentationResponse {
+ presentations: resolved_color
+ .color_presentations
+ .into_iter()
+ .map(|presentation| proto::ColorPresentation {
+ label: presentation.label,
+ text_edit: presentation.text_edit.map(serialize_lsp_edit),
+ additional_text_edits: presentation
+ .additional_text_edits
+ .into_iter()
+ .map(serialize_lsp_edit)
+ .collect(),
+ })
+ .collect(),
+ })
+ }
+
async fn handle_resolve_inlay_hint(
this: Entity<Self>,
envelope: TypedEnvelope<proto::ResolveInlayHint>,
@@ -8829,7 +9382,7 @@ impl LspStore {
local.language_server_watched_paths.remove(&server_id);
let server_state = local.language_servers.remove(&server_id);
cx.notify();
- self.remove_result_ids(server_id);
+ self.cleanup_lsp_data(server_id);
cx.emit(LspStoreEvent::LanguageServerRemoved(server_id));
cx.spawn(async move |_, cx| {
Self::shutdown_language_server(server_state, name, cx).await;
@@ -9718,7 +10271,10 @@ impl LspStore {
}
}
- fn remove_result_ids(&mut self, for_server: LanguageServerId) {
+ fn cleanup_lsp_data(&mut self, for_server: LanguageServerId) {
+ if let Some(lsp_data) = &mut self.lsp_data {
+ lsp_data.buffer_lsp_data.remove(&for_server);
+ }
if let Some(local) = self.as_local_mut() {
local.buffer_pull_diagnostics_result_ids.remove(&for_server);
}
@@ -768,6 +768,21 @@ pub struct DirectoryItem {
pub is_dir: bool,
}
+#[derive(Clone, Debug, PartialEq)]
+pub struct DocumentColor {
+ pub lsp_range: lsp::Range,
+ pub color: lsp::Color,
+ pub resolved: bool,
+ pub color_presentations: Vec<ColorPresentation>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct ColorPresentation {
+ pub label: String,
+ pub text_edit: Option<lsp::TextEdit>,
+ pub additional_text_edits: Vec<lsp::TextEdit>,
+}
+
#[derive(Clone)]
pub enum DirectoryLister {
Project(Entity<Project>),
@@ -3721,16 +3736,6 @@ impl Project {
})
}
- pub fn document_diagnostics(
- &mut self,
- buffer_handle: Entity<Buffer>,
- cx: &mut Context<Self>,
- ) -> Task<Result<Vec<LspPullDiagnostics>>> {
- self.lsp_store.update(cx, |lsp_store, cx| {
- lsp_store.pull_diagnostics(buffer_handle, cx)
- })
- }
-
pub fn update_diagnostics(
&mut self,
language_server_id: LanguageServerId,
@@ -666,6 +666,51 @@ message LanguageServerPromptResponse {
optional uint64 action_response = 1;
}
+message GetDocumentColor {
+ uint64 project_id = 1;
+ uint64 buffer_id = 2;
+ repeated VectorClockEntry version = 3;
+
+}
+
+message GetDocumentColorResponse {
+ repeated ColorInformation colors = 1;
+ repeated VectorClockEntry version = 2;
+
+}
+
+message ColorInformation {
+ PointUtf16 lsp_range_start = 1;
+ PointUtf16 lsp_range_end = 2;
+ float red = 3;
+ float green = 4;
+ float blue = 5;
+ float alpha = 6;
+}
+
+message GetColorPresentation {
+ uint64 project_id = 1;
+ uint64 buffer_id = 2;
+ ColorInformation color = 3;
+ uint64 server_id = 4;
+}
+
+message GetColorPresentationResponse {
+ repeated ColorPresentation presentations = 1;
+}
+
+message ColorPresentation {
+ string label = 1;
+ optional TextEdit text_edit = 2;
+ repeated TextEdit additional_text_edits = 3;
+}
+
+message TextEdit {
+ string new_text = 1;
+ PointUtf16 lsp_range_start = 2;
+ PointUtf16 lsp_range_end = 3;
+}
+
message MultiLspQuery {
uint64 project_id = 1;
uint64 buffer_id = 2;
@@ -679,6 +724,7 @@ message MultiLspQuery {
GetSignatureHelp get_signature_help = 7;
GetCodeLens get_code_lens = 8;
GetDocumentDiagnostics get_document_diagnostics = 9;
+ GetDocumentColor get_document_color = 10;
}
}
@@ -705,7 +751,9 @@ message LspResponse {
GetSignatureHelpResponse get_signature_help_response = 3;
GetCodeLensResponse get_code_lens_response = 4;
GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5;
+ GetDocumentColorResponse get_document_color_response = 6;
}
+ uint64 server_id = 7;
}
message LanguageServerIdForName {
@@ -391,7 +391,12 @@ message Envelope {
GetDocumentDiagnostics get_document_diagnostics = 350;
GetDocumentDiagnosticsResponse get_document_diagnostics_response = 351;
- PullWorkspaceDiagnostics pull_workspace_diagnostics = 352; // current max
+ PullWorkspaceDiagnostics pull_workspace_diagnostics = 352;
+
+ GetDocumentColor get_document_color = 353;
+ GetDocumentColorResponse get_document_color_response = 354;
+ GetColorPresentation get_color_presentation = 355;
+ GetColorPresentationResponse get_color_presentation_response = 356; // current max
}
@@ -221,6 +221,10 @@ messages!(
(ResolveCompletionDocumentationResponse, Background),
(ResolveInlayHint, Background),
(ResolveInlayHintResponse, Background),
+ (GetDocumentColor, Background),
+ (GetDocumentColorResponse, Background),
+ (GetColorPresentation, Background),
+ (GetColorPresentationResponse, Background),
(RefreshCodeLens, Background),
(GetCodeLens, Background),
(GetCodeLensResponse, Background),
@@ -400,6 +404,8 @@ request_messages!(
ResolveCompletionDocumentationResponse
),
(ResolveInlayHint, ResolveInlayHintResponse),
+ (GetDocumentColor, GetDocumentColorResponse),
+ (GetColorPresentation, GetColorPresentationResponse),
(RespondToChannelInvite, Ack),
(RespondToContactRequest, Ack),
(SaveBuffer, BufferSaved),
@@ -487,9 +493,11 @@ entity_messages!(
BufferSaved,
CloseBuffer,
Commit,
+ GetColorPresentation,
CopyProjectEntry,
CreateBufferForPeer,
CreateProjectEntry,
+ GetDocumentColor,
DeleteProjectEntry,
ExpandProjectEntry,
ExpandAllForProjectEntry,
@@ -1072,7 +1072,7 @@ impl Element for TerminalElement {
color: *color,
corner_radius: 0.15 * layout.dimensions.line_height,
};
- hr.paint(bounds, window);
+ hr.paint(true, bounds, window);
}
}