Show inline previews for LSP document colors (#32816)

https://github.com/user-attachments/assets/ad0fa304-e4fb-4598-877d-c02141f35d6f

Closes https://github.com/zed-industries/zed/issues/4678

Also adds the code to support `textDocument/colorPresentation`
counterpart that serves as a resolve mechanism for the document colors.
The resolve itself is not run though, and the editor does not
accommodate color presentations in the editor yet — until a well
described use case is provided.

Use `lsp_document_colors` editor settings to alter the presentation and
turn the feature off.

Release Notes:

- Start showing inline previews for LSP document colors
This commit is contained in:
Kirill Bulatov 2025-06-17 16:46:21 +03:00 committed by GitHub
parent acb0210d26
commit f46957584f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1796 additions and 268 deletions

View file

@ -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.

View file

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

View file

@ -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(),

View file

@ -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,
);
}

View file

@ -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,
);
});

View file

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

View file

@ -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(

View file

@ -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

View file

@ -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,

View file

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

View file

@ -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();
}
}

View file

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

View file

@ -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(()))
}
}

View file

@ -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,
})
}

View file

@ -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!({

View file

@ -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,

View file

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

View file

@ -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,

View file

@ -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 {

View file

@ -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
}

View file

@ -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,

View file

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