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

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