diff --git a/assets/settings/default.json b/assets/settings/default.json index a1420a9aee..578cf613e5 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -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. diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 8adc40a9f7..6b84ca998e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -323,6 +323,7 @@ impl Server { .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index c9855c2fde..56d10b2a8d 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -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::(cx, |settings| { + settings.lsp_document_colors = Some(DocumentColorsRenderMode::None); + }); + }); + }); + cx_b.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings::(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::() + .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::(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::::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::() + .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::::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::(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::::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::::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::(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::::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::(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::::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 { labels } +#[track_caller] +fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec { + editor + .all_inlays(cx) + .into_iter() + .filter_map(|inlay| inlay.get_color()) + .map(Rgba::from) + .collect() +} + fn blame_entry(sha: &str, range: Range) -> git::blame::BlameEntry { git::blame::BlameEntry { sha: sha.parse().unwrap(), diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 83505d21b3..0d47eaf367 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -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, ); } diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index d755086024..237dfffb0f 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -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, ); }); diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 14fa92172f..f8f2bf93c5 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -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, } 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) -> 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>(id: usize, position: Anchor, text: T) -> Self { + pub fn debugger>(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 { + 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::(); - 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"); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 25b7f24e20..2c1d9ab44e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, } #[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 { + self.display_map + .read(cx) + .current_inlays() + .cloned() + .collect() + } + fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context) { 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::>() }) 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, + for_buffer: Option, + 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>>; - - fn pull_diagnostics_for_buffer( - &self, - buffer: Entity, - cx: &mut App, - ) -> Task>; } pub trait CompletionProvider { @@ -21460,85 +21498,6 @@ impl SemanticsProvider for Entity { project.perform_rename(buffer.clone(), position, new_name, cx) })) } - - fn pull_diagnostics_for_buffer( - &self, - buffer: Entity, - cx: &mut App, - ) -> Task> { - 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( diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index df859a03e8..724cf5d905 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -50,6 +50,22 @@ pub struct EditorSettings { pub diagnostics_max_severity: Option, 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, + + /// How to render LSP `textDocument/documentColor` colors in the editor. + /// + /// Default: [`DocumentColorsRenderMode::Inlay`] + pub lsp_document_colors: Option, } // Toolbar related settings diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 930f81f9d1..596942272d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b08c0d6bdc..a42a6734fc 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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, + 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, + document_colors: Option<(DocumentColorsRenderMode, Vec<(Range, Hsla)>)>, } impl EditorLayout { @@ -9735,17 +9791,18 @@ pub struct HighlightedRangeLine { } impl HighlightedRange { - pub fn paint(&self, bounds: Bounds, window: &mut Window) { + pub fn paint(&self, fill: bool, bounds: Bounds, 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, 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); diff --git a/crates/editor/src/lsp_colors.rs b/crates/editor/src/lsp_colors.rs new file mode 100644 index 0000000000..c7a2f0013d --- /dev/null +++ b/crates/editor/src/lsp_colors.rs @@ -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, DocumentColor, InlayId)>, + inlay_colors: HashMap, + 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 { + 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, 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, 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, + buffer_id: Option, + _: &Window, + cx: &mut Context, + ) { + 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::>() + }) + .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::>() + }); + 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, 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(); + } +} diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 75645bbaa2..e4167ee68e 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -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(); diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 06ace151bf..c5f937f20c 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -522,12 +522,4 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { ) -> Option>> { None } - - fn pull_diagnostics_for_buffer( - &self, - _: Entity, - _: &mut App, - ) -> Task> { - Task::ready(Ok(())) - } } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index c3b91bae31..18f6bb8709 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -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::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 { + 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, + }) +} diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 39d85c3432..8e29987c20 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -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!({ diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 4f43ae8b25..e22dd736ef 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -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; + 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, + _: &App, + ) -> Result { + 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, + _: Entity, + _: Entity, + _: LanguageServerId, + _: AsyncApp, + ) -> Result { + 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, + _: Entity, + _: AsyncApp, + ) -> Result { + 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, + _: Entity, + _: AsyncApp, + ) -> Result { + 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::new(message.buffer_id) + } +} + fn process_related_documents( diagnostics: &mut HashMap, server_id: LanguageServerId, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 46c8185814..99f37556af 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -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, HashMap>>, + lsp_data: Option, +} + +type DocumentColorTask = Shared, Arc>>>; + +#[derive(Debug)] +struct LspData { + mtime: MTime, + buffer_lsp_data: HashMap>, + colors_update: HashMap, + last_version_queried: HashMap, +} + +#[derive(Debug, Default)] +struct BufferLspData { + colors: Option>, } 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, ) { 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, + server_id: LanguageServerId, + cx: &mut Context, + ) -> Task> { + 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::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, @@ -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::, 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, + cx: &mut Context, + ) -> Task> { + 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, + buffer: Entity, + cx: &mut Context, + ) -> Option { + 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::>(); + + 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, + cx: &mut Context, + ) -> Task)>>> { + 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::, 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( &mut self, buffer: &Entity, @@ -5937,7 +6357,7 @@ impl LspStore { all_actions_task .await .into_iter() - .flatten() + .flat_map(|(_, actions)| actions) .filter(|help| !help.label.is_empty()) .collect::>() }) @@ -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::>() }) } @@ -6948,7 +7368,7 @@ impl LspStore { position: Option

, request: R, cx: &mut Context, - ) -> Task> + ) -> Task> 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::>(); 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::>() })?; 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::, + 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, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + 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, envelope: TypedEnvelope, @@ -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); } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fc345bb315..d0f266134e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -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, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ColorPresentation { + pub label: String, + pub text_edit: Option, + pub additional_text_edits: Vec, +} + #[derive(Clone)] pub enum DirectoryLister { Project(Entity), @@ -3721,16 +3736,6 @@ impl Project { }) } - pub fn document_diagnostics( - &mut self, - buffer_handle: Entity, - cx: &mut Context, - ) -> Task>> { - 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, diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 65d9555847..71831759e5 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -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 { diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index a23381508a..31f929ec90 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -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 } diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 3f72415dd7..e3aeb557ab 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -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, diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 155073c1ef..c0671048f6 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -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); } }