Merge pull request #483 from zed-industries/document-highlights

Show document highlights from the language server when moving the cursor
This commit is contained in:
Antonio Scandurra 2022-02-23 10:38:15 +01:00 committed by GitHub
commit 6f77ede38e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 472 additions and 49 deletions

View file

@ -442,6 +442,7 @@ pub struct Editor {
next_completion_id: CompletionId,
available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
code_actions_task: Option<Task<()>>,
document_highlights_task: Option<Task<()>>,
pending_rename: Option<RenameState>,
}
@ -903,6 +904,7 @@ impl Editor {
next_completion_id: 0,
available_code_actions: Default::default(),
code_actions_task: Default::default(),
document_highlights_task: Default::default(),
pending_rename: Default::default(),
};
this.end_selection(cx);
@ -2295,6 +2297,76 @@ impl Editor {
None
}
fn refresh_document_highlights(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
let project = self.project.as_ref()?;
let buffer = self.buffer.read(cx);
let newest_selection = self.newest_anchor_selection().clone();
let cursor_position = newest_selection.head();
let (cursor_buffer, cursor_buffer_position) =
buffer.text_anchor_for_position(cursor_position.clone(), cx)?;
let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?;
if cursor_buffer != tail_buffer {
return None;
}
let highlights = project.update(cx, |project, cx| {
project.document_highlights(&cursor_buffer, cursor_buffer_position, cx)
});
enum DocumentHighlightRead {}
enum DocumentHighlightWrite {}
self.document_highlights_task = Some(cx.spawn_weak(|this, mut cx| async move {
let highlights = highlights.log_err().await;
if let Some((this, highlights)) = this.upgrade(&cx).zip(highlights) {
this.update(&mut cx, |this, cx| {
let buffer_id = cursor_position.buffer_id;
let excerpt_id = cursor_position.excerpt_id.clone();
let settings = (this.build_settings)(cx);
let buffer = this.buffer.read(cx);
if !buffer
.text_anchor_for_position(cursor_position, cx)
.map_or(false, |(buffer, _)| buffer == cursor_buffer)
{
return;
}
let mut write_ranges = Vec::new();
let mut read_ranges = Vec::new();
for highlight in highlights {
let range = Anchor {
buffer_id,
excerpt_id: excerpt_id.clone(),
text_anchor: highlight.range.start,
}..Anchor {
buffer_id,
excerpt_id: excerpt_id.clone(),
text_anchor: highlight.range.end,
};
if highlight.kind == lsp::DocumentHighlightKind::WRITE {
write_ranges.push(range);
} else {
read_ranges.push(range);
}
}
this.highlight_ranges::<DocumentHighlightRead>(
read_ranges,
settings.style.document_highlight_read_background,
cx,
);
this.highlight_ranges::<DocumentHighlightWrite>(
write_ranges,
settings.style.document_highlight_write_background,
cx,
);
cx.notify();
});
}
}));
None
}
pub fn render_code_actions_indicator(&self, cx: &mut ViewContext<Self>) -> Option<ElementBox> {
if self.available_code_actions.is_some() {
enum Tag {}
@ -4840,6 +4912,7 @@ impl Editor {
self.available_code_actions.take();
}
self.refresh_code_actions(cx);
self.refresh_document_highlights(cx);
self.pause_cursor_blinking(cx);
cx.emit(Event::SelectionsChanged);
@ -5294,6 +5367,8 @@ impl EditorSettings {
highlighted_line_background: Default::default(),
diff_background_deleted: Default::default(),
diff_background_inserted: Default::default(),
document_highlight_read_background: Default::default(),
document_highlight_write_background: Default::default(),
line_number: Default::default(),
line_number_active: Default::default(),
selection: Default::default(),

View file

@ -1,4 +1,4 @@
use crate::{Location, Project, ProjectTransaction};
use crate::{DocumentHighlight, Location, Project, ProjectTransaction};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use client::{proto, PeerId};
@ -8,7 +8,8 @@ use language::{
proto::{deserialize_anchor, serialize_anchor},
range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, ToPointUtf16,
};
use std::{ops::Range, path::Path};
use lsp::DocumentHighlightKind;
use std::{cmp::Reverse, ops::Range, path::Path};
#[async_trait(?Send)]
pub(crate) trait LspCommand: 'static + Sized {
@ -70,6 +71,10 @@ pub(crate) struct GetReferences {
pub position: PointUtf16,
}
pub(crate) struct GetDocumentHighlights {
pub position: PointUtf16,
}
#[async_trait(?Send)]
impl LspCommand for PrepareRename {
type Response = Option<Range<Anchor>>;
@ -598,3 +603,138 @@ impl LspCommand for GetReferences {
message.buffer_id
}
}
#[async_trait(?Send)]
impl LspCommand for GetDocumentHighlights {
type Response = Vec<DocumentHighlight>;
type LspRequest = lsp::request::DocumentHighlightRequest;
type ProtoRequest = proto::GetDocumentHighlights;
fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::DocumentHighlightParams {
lsp::DocumentHighlightParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document: lsp::TextDocumentIdentifier {
uri: lsp::Url::from_file_path(path).unwrap(),
},
position: self.position.to_lsp_position(),
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
}
}
async fn response_from_lsp(
self,
lsp_highlights: Option<Vec<lsp::DocumentHighlight>>,
_: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
cx: AsyncAppContext,
) -> Result<Vec<DocumentHighlight>> {
buffer.read_with(&cx, |buffer, _| {
let mut lsp_highlights = lsp_highlights.unwrap_or_default();
lsp_highlights.sort_unstable_by_key(|h| (h.range.start, Reverse(h.range.end)));
Ok(lsp_highlights
.into_iter()
.map(|lsp_highlight| {
let start = buffer
.clip_point_utf16(point_from_lsp(lsp_highlight.range.start), Bias::Left);
let end = buffer
.clip_point_utf16(point_from_lsp(lsp_highlight.range.end), Bias::Left);
DocumentHighlight {
range: buffer.anchor_after(start)..buffer.anchor_before(end),
kind: lsp_highlight
.kind
.unwrap_or(lsp::DocumentHighlightKind::READ),
}
})
.collect())
})
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentHighlights {
proto::GetDocumentHighlights {
project_id,
buffer_id: buffer.remote_id(),
position: Some(language::proto::serialize_anchor(
&buffer.anchor_before(self.position),
)),
}
}
fn from_proto(
message: proto::GetDocumentHighlights,
_: &mut Project,
buffer: &Buffer,
) -> Result<Self> {
let position = message
.position
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("invalid position"))?;
if !buffer.can_resolve(&position) {
Err(anyhow!("cannot resolve position"))?;
}
Ok(Self {
position: position.to_point_utf16(buffer),
})
}
fn response_to_proto(
response: Vec<DocumentHighlight>,
_: &mut Project,
_: PeerId,
_: &clock::Global,
_: &AppContext,
) -> proto::GetDocumentHighlightsResponse {
let highlights = response
.into_iter()
.map(|highlight| proto::DocumentHighlight {
start: Some(serialize_anchor(&highlight.range.start)),
end: Some(serialize_anchor(&highlight.range.end)),
kind: match highlight.kind {
DocumentHighlightKind::TEXT => proto::document_highlight::Kind::Text.into(),
DocumentHighlightKind::WRITE => proto::document_highlight::Kind::Write.into(),
DocumentHighlightKind::READ => proto::document_highlight::Kind::Read.into(),
_ => proto::document_highlight::Kind::Text.into(),
},
})
.collect();
proto::GetDocumentHighlightsResponse { highlights }
}
async fn response_from_proto(
self,
message: proto::GetDocumentHighlightsResponse,
_: ModelHandle<Project>,
_: ModelHandle<Buffer>,
_: AsyncAppContext,
) -> Result<Vec<DocumentHighlight>> {
Ok(message
.highlights
.into_iter()
.map(|highlight| {
let start = highlight
.start
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target start"))?;
let end = highlight
.end
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("missing target end"))?;
let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,
Some(proto::document_highlight::Kind::Read) => DocumentHighlightKind::READ,
Some(proto::document_highlight::Kind::Write) => DocumentHighlightKind::WRITE,
None => DocumentHighlightKind::TEXT,
};
Ok(DocumentHighlight {
range: start..end,
kind,
})
})
.collect::<Result<Vec<_>>>()?)
}
fn buffer_id_from_proto(message: &proto::GetDocumentHighlights) -> u64 {
message.buffer_id
}
}

View file

@ -18,7 +18,7 @@ use language::{
Diagnostic, DiagnosticEntry, File as _, Language, LanguageRegistry, Operation, PointUtf16,
ToLspPosition, ToOffset, ToPointUtf16, Transaction,
};
use lsp::{DiagnosticSeverity, LanguageServer};
use lsp::{DiagnosticSeverity, DocumentHighlightKind, LanguageServer};
use lsp_command::*;
use postage::{broadcast, prelude::Stream, sink::Sink, watch};
use rand::prelude::*;
@ -123,6 +123,12 @@ pub struct Location {
pub range: Range<language::Anchor>,
}
#[derive(Debug)]
pub struct DocumentHighlight {
pub range: Range<language::Anchor>,
pub kind: DocumentHighlightKind,
}
#[derive(Clone, Debug)]
pub struct Symbol {
pub source_worktree_id: WorktreeId,
@ -202,6 +208,7 @@ impl Project {
client.add_entity_request_handler(Self::handle_get_code_actions);
client.add_entity_request_handler(Self::handle_get_completions);
client.add_entity_request_handler(Self::handle_lsp_command::<GetDefinition>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetReferences>);
client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
@ -1269,6 +1276,16 @@ impl Project {
self.request_lsp(buffer.clone(), GetReferences { position }, cx)
}
pub fn document_highlights<T: ToPointUtf16>(
&self,
buffer: &ModelHandle<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<DocumentHighlight>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.request_lsp(buffer.clone(), GetDocumentHighlights { position }, cx)
}
pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
if self.is_local() {
let mut language_servers = HashMap::default();

View file

@ -25,57 +25,59 @@ message Envelope {
GetDefinitionResponse get_definition_response = 19;
GetReferences get_references = 20;
GetReferencesResponse get_references_response = 21;
GetProjectSymbols get_project_symbols = 22;
GetProjectSymbolsResponse get_project_symbols_response = 23;
OpenBufferForSymbol open_buffer_for_symbol = 24;
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 25;
GetDocumentHighlights get_document_highlights = 22;
GetDocumentHighlightsResponse get_document_highlights_response = 23;
GetProjectSymbols get_project_symbols = 24;
GetProjectSymbolsResponse get_project_symbols_response = 25;
OpenBufferForSymbol open_buffer_for_symbol = 26;
OpenBufferForSymbolResponse open_buffer_for_symbol_response = 27;
RegisterWorktree register_worktree = 26;
UnregisterWorktree unregister_worktree = 27;
ShareWorktree share_worktree = 28;
UpdateWorktree update_worktree = 29;
UpdateDiagnosticSummary update_diagnostic_summary = 30;
DiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 31;
DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 32;
RegisterWorktree register_worktree = 28;
UnregisterWorktree unregister_worktree = 29;
ShareWorktree share_worktree = 30;
UpdateWorktree update_worktree = 31;
UpdateDiagnosticSummary update_diagnostic_summary = 32;
DiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 33;
DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 34;
OpenBuffer open_buffer = 33;
OpenBufferResponse open_buffer_response = 34;
CloseBuffer close_buffer = 35;
UpdateBuffer update_buffer = 36;
UpdateBufferFile update_buffer_file = 37;
SaveBuffer save_buffer = 38;
BufferSaved buffer_saved = 39;
BufferReloaded buffer_reloaded = 40;
FormatBuffers format_buffers = 41;
FormatBuffersResponse format_buffers_response = 42;
GetCompletions get_completions = 43;
GetCompletionsResponse get_completions_response = 44;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 45;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 46;
GetCodeActions get_code_actions = 47;
GetCodeActionsResponse get_code_actions_response = 48;
ApplyCodeAction apply_code_action = 49;
ApplyCodeActionResponse apply_code_action_response = 50;
PrepareRename prepare_rename = 51;
PrepareRenameResponse prepare_rename_response = 52;
PerformRename perform_rename = 53;
PerformRenameResponse perform_rename_response = 54;
OpenBuffer open_buffer = 35;
OpenBufferResponse open_buffer_response = 36;
CloseBuffer close_buffer = 37;
UpdateBuffer update_buffer = 38;
UpdateBufferFile update_buffer_file = 39;
SaveBuffer save_buffer = 40;
BufferSaved buffer_saved = 41;
BufferReloaded buffer_reloaded = 42;
FormatBuffers format_buffers = 43;
FormatBuffersResponse format_buffers_response = 44;
GetCompletions get_completions = 45;
GetCompletionsResponse get_completions_response = 46;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 47;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 48;
GetCodeActions get_code_actions = 49;
GetCodeActionsResponse get_code_actions_response = 50;
ApplyCodeAction apply_code_action = 51;
ApplyCodeActionResponse apply_code_action_response = 52;
PrepareRename prepare_rename = 53;
PrepareRenameResponse prepare_rename_response = 54;
PerformRename perform_rename = 55;
PerformRenameResponse perform_rename_response = 56;
GetChannels get_channels = 55;
GetChannelsResponse get_channels_response = 56;
JoinChannel join_channel = 57;
JoinChannelResponse join_channel_response = 58;
LeaveChannel leave_channel = 59;
SendChannelMessage send_channel_message = 60;
SendChannelMessageResponse send_channel_message_response = 61;
ChannelMessageSent channel_message_sent = 62;
GetChannelMessages get_channel_messages = 63;
GetChannelMessagesResponse get_channel_messages_response = 64;
GetChannels get_channels = 57;
GetChannelsResponse get_channels_response = 58;
JoinChannel join_channel = 59;
JoinChannelResponse join_channel_response = 60;
LeaveChannel leave_channel = 61;
SendChannelMessage send_channel_message = 62;
SendChannelMessageResponse send_channel_message_response = 63;
ChannelMessageSent channel_message_sent = 64;
GetChannelMessages get_channel_messages = 65;
GetChannelMessagesResponse get_channel_messages_response = 66;
UpdateContacts update_contacts = 65;
UpdateContacts update_contacts = 67;
GetUsers get_users = 66;
GetUsersResponse get_users_response = 67;
GetUsers get_users = 68;
GetUsersResponse get_users_response = 69;
}
}
@ -181,12 +183,34 @@ message GetReferencesResponse {
repeated Location locations = 1;
}
message GetDocumentHighlights {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
}
message GetDocumentHighlightsResponse {
repeated DocumentHighlight highlights = 1;
}
message Location {
Buffer buffer = 1;
Anchor start = 2;
Anchor end = 3;
}
message DocumentHighlight {
Kind kind = 1;
Anchor start = 2;
Anchor end = 3;
enum Kind {
Text = 0;
Read = 1;
Write = 2;
}
}
message GetProjectSymbols {
uint64 project_id = 1;
string query = 2;

View file

@ -157,6 +157,8 @@ messages!(
(GetCompletionsResponse, Foreground),
(GetDefinition, Foreground),
(GetDefinitionResponse, Foreground),
(GetDocumentHighlights, Foreground),
(GetDocumentHighlightsResponse, Foreground),
(GetReferences, Foreground),
(GetReferencesResponse, Foreground),
(GetProjectSymbols, Background),
@ -210,6 +212,7 @@ request_messages!(
(GetCodeActions, GetCodeActionsResponse),
(GetCompletions, GetCompletionsResponse),
(GetDefinition, GetDefinitionResponse),
(GetDocumentHighlights, GetDocumentHighlightsResponse),
(GetReferences, GetReferencesResponse),
(GetProjectSymbols, GetProjectSymbolsResponse),
(GetUsers, GetUsersResponse),
@ -245,6 +248,7 @@ entity_messages!(
GetCodeActions,
GetCompletions,
GetDefinition,
GetDocumentHighlights,
GetReferences,
GetProjectSymbols,
JoinProject,

View file

@ -80,6 +80,7 @@ impl Server {
.add_message_handler(Server::disk_based_diagnostics_updated)
.add_request_handler(Server::get_definition)
.add_request_handler(Server::get_references)
.add_request_handler(Server::get_document_highlights)
.add_request_handler(Server::get_project_symbols)
.add_request_handler(Server::open_buffer_for_symbol)
.add_request_handler(Server::open_buffer)
@ -604,6 +605,20 @@ impl Server {
.await?)
}
async fn get_document_highlights(
self: Arc<Server>,
request: TypedEnvelope<proto::GetDocumentHighlights>,
) -> tide::Result<proto::GetDocumentHighlightsResponse> {
let host_connection_id = self
.state()
.read_project(request.payload.project_id, request.sender_id)?
.host_connection_id;
Ok(self
.peer
.forward_request(request.sender_id, host_connection_id, request.payload)
.await?)
}
async fn get_project_symbols(
self: Arc<Server>,
request: TypedEnvelope<proto::GetProjectSymbols>,
@ -2855,6 +2870,148 @@ mod tests {
});
}
#[gpui::test(iterations = 10)]
async fn test_document_highlights(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
cx_a.foreground().forbid_parking();
let lang_registry = Arc::new(LanguageRegistry::new());
let fs = FakeFs::new(cx_a.background());
fs.insert_tree(
"/root-1",
json!({
".zed.toml": r#"collaborators = ["user_b"]"#,
"main.rs": "fn double(number: i32) -> i32 { number + number }",
}),
)
.await;
// Set up a fake language server.
let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
lang_registry.add(Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
language_server: Some(language_server_config),
..Default::default()
},
Some(tree_sitter_rust::language()),
)));
// Connect to a server as 2 clients.
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let client_a = server.create_client(&mut cx_a, "user_a").await;
let client_b = server.create_client(&mut cx_b, "user_b").await;
// Share a project as client A
let project_a = cx_a.update(|cx| {
Project::local(
client_a.clone(),
client_a.user_store.clone(),
lang_registry.clone(),
fs.clone(),
cx,
)
});
let (worktree_a, _) = project_a
.update(&mut cx_a, |p, cx| {
p.find_or_create_local_worktree("/root-1", false, cx)
})
.await
.unwrap();
worktree_a
.read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await;
let worktree_id = worktree_a.read_with(&cx_a, |tree, _| tree.id());
project_a
.update(&mut cx_a, |p, cx| p.share(cx))
.await
.unwrap();
// Join the worktree as client B.
let project_b = Project::remote(
project_id,
client_b.clone(),
client_b.user_store.clone(),
lang_registry.clone(),
fs.clone(),
&mut cx_b.to_async(),
)
.await
.unwrap();
// Open the file on client B.
let buffer_b = cx_b
.background()
.spawn(project_b.update(&mut cx_b, |p, cx| {
p.open_buffer((worktree_id, "main.rs"), cx)
}))
.await
.unwrap();
// Request document highlights as the guest.
let highlights =
project_b.update(&mut cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx));
let mut fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _>(
|params| {
assert_eq!(
params
.text_document_position_params
.text_document
.uri
.as_str(),
"file:///root-1/main.rs"
);
assert_eq!(
params.text_document_position_params.position,
lsp::Position::new(0, 34)
);
Some(vec![
lsp::DocumentHighlight {
kind: Some(lsp::DocumentHighlightKind::WRITE),
range: lsp::Range::new(
lsp::Position::new(0, 10),
lsp::Position::new(0, 16),
),
},
lsp::DocumentHighlight {
kind: Some(lsp::DocumentHighlightKind::READ),
range: lsp::Range::new(
lsp::Position::new(0, 32),
lsp::Position::new(0, 38),
),
},
lsp::DocumentHighlight {
kind: Some(lsp::DocumentHighlightKind::READ),
range: lsp::Range::new(
lsp::Position::new(0, 41),
lsp::Position::new(0, 47),
),
},
])
},
);
let highlights = highlights.await.unwrap();
buffer_b.read_with(&cx_b, |buffer, _| {
let snapshot = buffer.snapshot();
let highlights = highlights
.into_iter()
.map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
.collect::<Vec<_>>();
assert_eq!(
highlights,
&[
(lsp::DocumentHighlightKind::WRITE, 10..16),
(lsp::DocumentHighlightKind::READ, 32..38),
(lsp::DocumentHighlightKind::READ, 41..47)
]
)
});
}
#[gpui::test(iterations = 10)]
async fn test_project_symbols(mut cx_a: TestAppContext, mut cx_b: TestAppContext) {
cx_a.foreground().forbid_parking();

View file

@ -279,6 +279,8 @@ pub struct EditorStyle {
pub gutter_padding_factor: f32,
pub active_line_background: Color,
pub highlighted_line_background: Color,
pub document_highlight_read_background: Color,
pub document_highlight_write_background: Color,
pub diff_background_deleted: Color,
pub diff_background_inserted: Color,
pub line_number: Color,
@ -386,6 +388,8 @@ impl InputEditorStyle {
gutter_padding_factor: Default::default(),
active_line_background: Default::default(),
highlighted_line_background: Default::default(),
document_highlight_read_background: Default::default(),
document_highlight_write_background: Default::default(),
diff_background_deleted: Default::default(),
diff_background_inserted: Default::default(),
line_number: Default::default(),

View file

@ -249,6 +249,8 @@ gutter_background = "$surface.1"
gutter_padding_factor = 2.5
active_line_background = "$state.active_line"
highlighted_line_background = "$state.highlighted_line"
document_highlight_read_background = "#99999920"
document_highlight_write_background = "#99999916"
diff_background_deleted = "$state.deleted_line"
diff_background_inserted = "$state.inserted_line"
line_number = "$text.2.color"