mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
lsp: Add support for linked editing range edits (HTML tag autorenaming) (#12769)
This PR adds support for [linked editing of ranges](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_linkedEditingRange), which in short means that editing one part of a file can now change related parts in that same file. Think of automatically renaming HTML/TSX closing tags when the opening one is changed. TODO: - [x] proto changes - [x] Allow disabling linked editing ranges on a per language basis. Fixes #4535 Release Notes: - Added support for linked editing ranges LSP request. Editing opening tags in HTML/TSX files (with vtsls) performs the same edit on the closing tag as well (and vice versa). It can be turned off on a language-by-language basis with the following setting: ``` "languages": { "HTML": { "linked_edits": true }, } ``` --------- Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
parent
98659eabf1
commit
b6ea393d14
@ -131,7 +131,14 @@
|
|||||||
// The default number of lines to expand excerpts in the multibuffer by.
|
// The default number of lines to expand excerpts in the multibuffer by.
|
||||||
"expand_excerpt_lines": 3,
|
"expand_excerpt_lines": 3,
|
||||||
// Globs to match against file paths to determine if a file is private.
|
// Globs to match against file paths to determine if a file is private.
|
||||||
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
|
"private_files": [
|
||||||
|
"**/.env*",
|
||||||
|
"**/*.pem",
|
||||||
|
"**/*.key",
|
||||||
|
"**/*.cert",
|
||||||
|
"**/*.crt",
|
||||||
|
"**/secrets.yml"
|
||||||
|
],
|
||||||
// Whether to use additional LSP queries to format (and amend) the code after
|
// Whether to use additional LSP queries to format (and amend) the code after
|
||||||
// every "trigger" symbol input, defined by LSP server capabilities.
|
// every "trigger" symbol input, defined by LSP server capabilities.
|
||||||
"use_on_type_format": true,
|
"use_on_type_format": true,
|
||||||
@ -354,6 +361,9 @@
|
|||||||
"show_call_status_icon": true,
|
"show_call_status_icon": true,
|
||||||
// Whether to use language servers to provide code intelligence.
|
// Whether to use language servers to provide code intelligence.
|
||||||
"enable_language_server": true,
|
"enable_language_server": true,
|
||||||
|
// Whether to perform linked edits of associated ranges, if the language server supports it.
|
||||||
|
// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
|
||||||
|
"linked_edits": true,
|
||||||
// The list of language servers to use (or disable) for all languages.
|
// The list of language servers to use (or disable) for all languages.
|
||||||
//
|
//
|
||||||
// This is typically customized on a per-language basis.
|
// This is typically customized on a per-language basis.
|
||||||
|
@ -548,6 +548,9 @@ impl Server {
|
|||||||
.add_request_handler(user_handler(
|
.add_request_handler(user_handler(
|
||||||
forward_mutating_project_request::<proto::RestartLanguageServers>,
|
forward_mutating_project_request::<proto::RestartLanguageServers>,
|
||||||
))
|
))
|
||||||
|
.add_request_handler(user_handler(
|
||||||
|
forward_mutating_project_request::<proto::LinkedEditingRange>,
|
||||||
|
))
|
||||||
.add_message_handler(create_buffer_for_peer)
|
.add_message_handler(create_buffer_for_peer)
|
||||||
.add_request_handler(update_buffer)
|
.add_request_handler(update_buffer)
|
||||||
.add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)
|
.add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)
|
||||||
|
@ -28,6 +28,7 @@ mod indent_guides;
|
|||||||
mod inlay_hint_cache;
|
mod inlay_hint_cache;
|
||||||
mod inline_completion_provider;
|
mod inline_completion_provider;
|
||||||
pub mod items;
|
pub mod items;
|
||||||
|
mod linked_editing_ranges;
|
||||||
mod mouse_context_menu;
|
mod mouse_context_menu;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
mod persistence;
|
mod persistence;
|
||||||
@ -88,6 +89,7 @@ use language::{
|
|||||||
Point, Selection, SelectionGoal, TransactionId,
|
Point, Selection, SelectionGoal, TransactionId,
|
||||||
};
|
};
|
||||||
use language::{BufferRow, Runnable, RunnableRange};
|
use language::{BufferRow, Runnable, RunnableRange};
|
||||||
|
use linked_editing_ranges::refresh_linked_ranges;
|
||||||
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
||||||
|
|
||||||
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
|
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
|
||||||
@ -478,6 +480,8 @@ pub struct Editor {
|
|||||||
available_code_actions: Option<(Location, Arc<[CodeAction]>)>,
|
available_code_actions: Option<(Location, Arc<[CodeAction]>)>,
|
||||||
code_actions_task: Option<Task<()>>,
|
code_actions_task: Option<Task<()>>,
|
||||||
document_highlights_task: Option<Task<()>>,
|
document_highlights_task: Option<Task<()>>,
|
||||||
|
linked_editing_range_task: Option<Task<Option<()>>>,
|
||||||
|
linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges,
|
||||||
pending_rename: Option<RenameState>,
|
pending_rename: Option<RenameState>,
|
||||||
searchable: bool,
|
searchable: bool,
|
||||||
cursor_shape: CursorShape,
|
cursor_shape: CursorShape,
|
||||||
@ -1768,6 +1772,7 @@ impl Editor {
|
|||||||
available_code_actions: Default::default(),
|
available_code_actions: Default::default(),
|
||||||
code_actions_task: Default::default(),
|
code_actions_task: Default::default(),
|
||||||
document_highlights_task: Default::default(),
|
document_highlights_task: Default::default(),
|
||||||
|
linked_editing_range_task: Default::default(),
|
||||||
pending_rename: Default::default(),
|
pending_rename: Default::default(),
|
||||||
searchable: true,
|
searchable: true,
|
||||||
cursor_shape: Default::default(),
|
cursor_shape: Default::default(),
|
||||||
@ -1828,6 +1833,7 @@ impl Editor {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
tasks_update_task: None,
|
tasks_update_task: None,
|
||||||
|
linked_edit_ranges: Default::default(),
|
||||||
previous_search_ranges: None,
|
previous_search_ranges: None,
|
||||||
};
|
};
|
||||||
this.tasks_update_task = Some(this.refresh_runnables(cx));
|
this.tasks_update_task = Some(this.refresh_runnables(cx));
|
||||||
@ -2208,7 +2214,6 @@ impl Editor {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let display_map = self
|
let display_map = self
|
||||||
.display_map
|
.display_map
|
||||||
.update(cx, |display_map, cx| display_map.snapshot(cx));
|
.update(cx, |display_map, cx| display_map.snapshot(cx));
|
||||||
@ -2296,6 +2301,7 @@ impl Editor {
|
|||||||
self.refresh_document_highlights(cx);
|
self.refresh_document_highlights(cx);
|
||||||
refresh_matching_bracket_highlights(self, cx);
|
refresh_matching_bracket_highlights(self, cx);
|
||||||
self.discard_inline_completion(false, cx);
|
self.discard_inline_completion(false, cx);
|
||||||
|
linked_editing_ranges::refresh_linked_ranges(self, cx);
|
||||||
if self.git_blame_inline_enabled {
|
if self.git_blame_inline_enabled {
|
||||||
self.start_inline_blame_timer(cx);
|
self.start_inline_blame_timer(cx);
|
||||||
}
|
}
|
||||||
@ -2307,7 +2313,6 @@ impl Editor {
|
|||||||
if self.selections.disjoint_anchors().len() == 1 {
|
if self.selections.disjoint_anchors().len() == 1 {
|
||||||
cx.emit(SearchEvent::ActiveMatchChanged)
|
cx.emit(SearchEvent::ActiveMatchChanged)
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2777,6 +2782,49 @@ impl Editor {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn linked_editing_ranges_for(
|
||||||
|
&self,
|
||||||
|
selection: Range<text::Anchor>,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Option<HashMap<Model<Buffer>, Vec<Range<text::Anchor>>>> {
|
||||||
|
if self.linked_edit_ranges.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ((base_range, linked_ranges), buffer_snapshot, buffer) =
|
||||||
|
selection.end.buffer_id.and_then(|end_buffer_id| {
|
||||||
|
if selection.start.buffer_id != Some(end_buffer_id) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let buffer = self.buffer.read(cx).buffer(end_buffer_id)?;
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
self.linked_edit_ranges
|
||||||
|
.get(end_buffer_id, selection.start..selection.end, &snapshot)
|
||||||
|
.map(|ranges| (ranges, snapshot, buffer))
|
||||||
|
})?;
|
||||||
|
use text::ToOffset as TO;
|
||||||
|
// find offset from the start of current range to current cursor position
|
||||||
|
let start_byte_offset = TO::to_offset(&base_range.start, &buffer_snapshot);
|
||||||
|
|
||||||
|
let start_offset = TO::to_offset(&selection.start, &buffer_snapshot);
|
||||||
|
let start_difference = start_offset - start_byte_offset;
|
||||||
|
let end_offset = TO::to_offset(&selection.end, &buffer_snapshot);
|
||||||
|
let end_difference = end_offset - start_byte_offset;
|
||||||
|
// Current range has associated linked ranges.
|
||||||
|
let mut linked_edits = HashMap::<_, Vec<_>>::default();
|
||||||
|
for range in linked_ranges.iter() {
|
||||||
|
let start_offset = TO::to_offset(&range.start, &buffer_snapshot);
|
||||||
|
let end_offset = start_offset + end_difference;
|
||||||
|
let start_offset = start_offset + start_difference;
|
||||||
|
let start = buffer_snapshot.anchor_after(start_offset);
|
||||||
|
let end = buffer_snapshot.anchor_after(end_offset);
|
||||||
|
linked_edits
|
||||||
|
.entry(buffer.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(start..end);
|
||||||
|
}
|
||||||
|
Some(linked_edits)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
||||||
let text: Arc<str> = text.into();
|
let text: Arc<str> = text.into();
|
||||||
|
|
||||||
@ -2787,6 +2835,7 @@ impl Editor {
|
|||||||
let selections = self.selections.all_adjusted(cx);
|
let selections = self.selections.all_adjusted(cx);
|
||||||
let mut brace_inserted = false;
|
let mut brace_inserted = false;
|
||||||
let mut edits = Vec::new();
|
let mut edits = Vec::new();
|
||||||
|
let mut linked_edits = HashMap::<_, Vec<_>>::default();
|
||||||
let mut new_selections = Vec::with_capacity(selections.len());
|
let mut new_selections = Vec::with_capacity(selections.len());
|
||||||
let mut new_autoclose_regions = Vec::new();
|
let mut new_autoclose_regions = Vec::new();
|
||||||
let snapshot = self.buffer.read(cx).read(cx);
|
let snapshot = self.buffer.read(cx).read(cx);
|
||||||
@ -2967,16 +3016,46 @@ impl Editor {
|
|||||||
// text with the given input and move the selection to the end of the
|
// text with the given input and move the selection to the end of the
|
||||||
// newly inserted text.
|
// newly inserted text.
|
||||||
let anchor = snapshot.anchor_after(selection.end);
|
let anchor = snapshot.anchor_after(selection.end);
|
||||||
|
if !self.linked_edit_ranges.is_empty() {
|
||||||
|
let start_anchor = snapshot.anchor_before(selection.start);
|
||||||
|
if let Some(ranges) =
|
||||||
|
self.linked_editing_ranges_for(start_anchor.text_anchor..anchor.text_anchor, cx)
|
||||||
|
{
|
||||||
|
for (buffer, edits) in ranges {
|
||||||
|
linked_edits
|
||||||
|
.entry(buffer.clone())
|
||||||
|
.or_default()
|
||||||
|
.extend(edits.into_iter().map(|range| (range, text.clone())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
new_selections.push((selection.map(|_| anchor), 0));
|
new_selections.push((selection.map(|_| anchor), 0));
|
||||||
edits.push((selection.start..selection.end, text.clone()));
|
edits.push((selection.start..selection.end, text.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(snapshot);
|
drop(snapshot);
|
||||||
|
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
this.buffer.update(cx, |buffer, cx| {
|
this.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.edit(edits, this.autoindent_mode.clone(), cx);
|
buffer.edit(edits, this.autoindent_mode.clone(), cx);
|
||||||
});
|
});
|
||||||
|
for (buffer, edits) in linked_edits {
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
let snapshot = buffer.snapshot();
|
||||||
|
let edits = edits
|
||||||
|
.into_iter()
|
||||||
|
.map(|(range, text)| {
|
||||||
|
use text::ToPoint as TP;
|
||||||
|
let end_point = TP::to_point(&range.end, &snapshot);
|
||||||
|
let start_point = TP::to_point(&range.start, &snapshot);
|
||||||
|
(start_point..end_point, text)
|
||||||
|
})
|
||||||
|
.sorted_by_key(|(range, _)| range.start)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
buffer.edit(edits, None, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
let new_anchor_selections = new_selections.iter().map(|e| &e.0);
|
let new_anchor_selections = new_selections.iter().map(|e| &e.0);
|
||||||
let new_selection_deltas = new_selections.iter().map(|e| e.1);
|
let new_selection_deltas = new_selections.iter().map(|e| e.1);
|
||||||
let snapshot = this.buffer.read(cx).read(cx);
|
let snapshot = this.buffer.read(cx).read(cx);
|
||||||
@ -3033,6 +3112,7 @@ impl Editor {
|
|||||||
|
|
||||||
let trigger_in_words = !had_active_inline_completion;
|
let trigger_in_words = !had_active_inline_completion;
|
||||||
this.trigger_completion_on_input(&text, trigger_in_words, cx);
|
this.trigger_completion_on_input(&text, trigger_in_words, cx);
|
||||||
|
linked_editing_ranges::refresh_linked_ranges(this, cx);
|
||||||
this.refresh_inline_completion(true, cx);
|
this.refresh_inline_completion(true, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -3970,6 +4050,7 @@ impl Editor {
|
|||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||||
let mut range_to_replace: Option<Range<isize>> = None;
|
let mut range_to_replace: Option<Range<isize>> = None;
|
||||||
let mut ranges = Vec::new();
|
let mut ranges = Vec::new();
|
||||||
|
let mut linked_edits = HashMap::<_, Vec<_>>::default();
|
||||||
for selection in &selections {
|
for selection in &selections {
|
||||||
if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) {
|
if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) {
|
||||||
let start = selection.start.saturating_sub(lookbehind);
|
let start = selection.start.saturating_sub(lookbehind);
|
||||||
@ -3999,6 +4080,21 @@ impl Editor {
|
|||||||
}));
|
}));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if !self.linked_edit_ranges.is_empty() {
|
||||||
|
let start_anchor = snapshot.anchor_before(selection.head());
|
||||||
|
let end_anchor = snapshot.anchor_after(selection.tail());
|
||||||
|
if let Some(ranges) = self
|
||||||
|
.linked_editing_ranges_for(start_anchor.text_anchor..end_anchor.text_anchor, cx)
|
||||||
|
{
|
||||||
|
for (buffer, edits) in ranges {
|
||||||
|
linked_edits.entry(buffer.clone()).or_default().extend(
|
||||||
|
edits
|
||||||
|
.into_iter()
|
||||||
|
.map(|range| (range, text[common_prefix_len..].to_owned())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let text = &text[common_prefix_len..];
|
let text = &text[common_prefix_len..];
|
||||||
|
|
||||||
@ -4025,6 +4121,22 @@ impl Editor {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
for (buffer, edits) in linked_edits {
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
let snapshot = buffer.snapshot();
|
||||||
|
let edits = edits
|
||||||
|
.into_iter()
|
||||||
|
.map(|(range, text)| {
|
||||||
|
use text::ToPoint as TP;
|
||||||
|
let end_point = TP::to_point(&range.end, &snapshot);
|
||||||
|
let start_point = TP::to_point(&range.start, &snapshot);
|
||||||
|
(start_point..end_point, text)
|
||||||
|
})
|
||||||
|
.sorted_by_key(|(range, _)| range.start)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
buffer.edit(edits, None, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.refresh_inline_completion(true, cx);
|
this.refresh_inline_completion(true, cx);
|
||||||
});
|
});
|
||||||
@ -5009,6 +5121,27 @@ impl Editor {
|
|||||||
pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
|
pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
this.select_autoclose_pair(cx);
|
this.select_autoclose_pair(cx);
|
||||||
|
let mut linked_ranges = HashMap::<_, Vec<_>>::default();
|
||||||
|
if !this.linked_edit_ranges.is_empty() {
|
||||||
|
let selections = this.selections.all::<MultiBufferPoint>(cx);
|
||||||
|
let snapshot = this.buffer.read(cx).snapshot(cx);
|
||||||
|
|
||||||
|
for selection in selections.iter() {
|
||||||
|
let selection_start = snapshot.anchor_before(selection.start).text_anchor;
|
||||||
|
let selection_end = snapshot.anchor_after(selection.end).text_anchor;
|
||||||
|
if selection_start.buffer_id != selection_end.buffer_id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(ranges) =
|
||||||
|
this.linked_editing_ranges_for(selection_start..selection_end, cx)
|
||||||
|
{
|
||||||
|
for (buffer, entries) in ranges {
|
||||||
|
linked_ranges.entry(buffer).or_default().extend(entries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut selections = this.selections.all::<MultiBufferPoint>(cx);
|
let mut selections = this.selections.all::<MultiBufferPoint>(cx);
|
||||||
if !this.selections.line_mode {
|
if !this.selections.line_mode {
|
||||||
let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
|
let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||||
@ -5049,7 +5182,33 @@ impl Editor {
|
|||||||
|
|
||||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||||
this.insert("", cx);
|
this.insert("", cx);
|
||||||
|
let empty_str: Arc<str> = Arc::from("");
|
||||||
|
for (buffer, edits) in linked_ranges {
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
use text::ToPoint as TP;
|
||||||
|
|
||||||
|
let edits = edits
|
||||||
|
.into_iter()
|
||||||
|
.map(|range| {
|
||||||
|
let end_point = TP::to_point(&range.end, &snapshot);
|
||||||
|
let mut start_point = TP::to_point(&range.start, &snapshot);
|
||||||
|
|
||||||
|
if end_point == start_point {
|
||||||
|
let offset = text::ToOffset::to_offset(&range.start, &snapshot)
|
||||||
|
.saturating_sub(1);
|
||||||
|
start_point = TP::to_point(&offset, &snapshot);
|
||||||
|
};
|
||||||
|
|
||||||
|
(start_point..end_point, empty_str.clone())
|
||||||
|
})
|
||||||
|
.sorted_by_key(|(range, _)| range.start)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
buffer.update(cx, |this, cx| {
|
||||||
|
this.edit(edits, None, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
this.refresh_inline_completion(true, cx);
|
this.refresh_inline_completion(true, cx);
|
||||||
|
linked_editing_ranges::refresh_linked_ranges(this, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10604,7 +10763,6 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
cx.emit(EditorEvent::BufferEdited);
|
cx.emit(EditorEvent::BufferEdited);
|
||||||
cx.emit(SearchEvent::MatchesInvalidated);
|
cx.emit(SearchEvent::MatchesInvalidated);
|
||||||
|
|
||||||
if *singleton_buffer_edited {
|
if *singleton_buffer_edited {
|
||||||
if let Some(project) = &self.project {
|
if let Some(project) = &self.project {
|
||||||
let project = project.read(cx);
|
let project = project.read(cx);
|
||||||
@ -10636,6 +10794,7 @@ impl Editor {
|
|||||||
|
|
||||||
let Some(project) = &self.project else { return };
|
let Some(project) = &self.project else { return };
|
||||||
let telemetry = project.read(cx).client().telemetry().clone();
|
let telemetry = project.read(cx).client().telemetry().clone();
|
||||||
|
refresh_linked_ranges(self, cx);
|
||||||
telemetry.log_edit_event("editor");
|
telemetry.log_edit_event("editor");
|
||||||
}
|
}
|
||||||
multi_buffer::Event::ExcerptsAdded {
|
multi_buffer::Event::ExcerptsAdded {
|
||||||
@ -10661,6 +10820,7 @@ impl Editor {
|
|||||||
cx.emit(EditorEvent::Reparsed);
|
cx.emit(EditorEvent::Reparsed);
|
||||||
}
|
}
|
||||||
multi_buffer::Event::LanguageChanged => {
|
multi_buffer::Event::LanguageChanged => {
|
||||||
|
linked_editing_ranges::refresh_linked_ranges(self, cx);
|
||||||
cx.emit(EditorEvent::Reparsed);
|
cx.emit(EditorEvent::Reparsed);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
150
crates/editor/src/linked_editing_ranges.rs
Normal file
150
crates/editor/src/linked_editing_ranges.rs
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use collections::HashMap;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use text::{AnchorRangeExt, BufferId, ToPoint};
|
||||||
|
use ui::ViewContext;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
use crate::Editor;
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub(super) struct LinkedEditingRanges(
|
||||||
|
/// Ranges are non-overlapping and sorted by .0 (thus, [x + 1].start > [x].end must hold)
|
||||||
|
pub HashMap<BufferId, Vec<(Range<text::Anchor>, Vec<Range<text::Anchor>>)>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl LinkedEditingRanges {
|
||||||
|
pub(super) fn get(
|
||||||
|
&self,
|
||||||
|
id: BufferId,
|
||||||
|
anchor: Range<text::Anchor>,
|
||||||
|
snapshot: &text::BufferSnapshot,
|
||||||
|
) -> Option<&(Range<text::Anchor>, Vec<Range<text::Anchor>>)> {
|
||||||
|
let ranges_for_buffer = self.0.get(&id)?;
|
||||||
|
let lower_bound = ranges_for_buffer
|
||||||
|
.partition_point(|(range, _)| range.start.cmp(&anchor.start, snapshot).is_le());
|
||||||
|
if lower_bound == 0 {
|
||||||
|
// None of the linked ranges contains `anchor`.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
ranges_for_buffer
|
||||||
|
.get(lower_bound - 1)
|
||||||
|
.filter(|(range, _)| range.end.cmp(&anchor.end, snapshot).is_ge())
|
||||||
|
}
|
||||||
|
pub(super) fn is_empty(&self) -> bool {
|
||||||
|
self.0.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(super) fn refresh_linked_ranges(this: &mut Editor, cx: &mut ViewContext<Editor>) -> Option<()> {
|
||||||
|
if this.pending_rename.is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let project = this.project.clone()?;
|
||||||
|
let buffer = this.buffer.read(cx);
|
||||||
|
let mut applicable_selections = vec![];
|
||||||
|
let selections = this.selections.all::<usize>(cx);
|
||||||
|
let snapshot = buffer.snapshot(cx);
|
||||||
|
for selection in selections {
|
||||||
|
let cursor_position = selection.head();
|
||||||
|
let start_position = snapshot.anchor_before(cursor_position);
|
||||||
|
let end_position = snapshot.anchor_after(selection.tail());
|
||||||
|
if start_position.buffer_id != end_position.buffer_id || end_position.buffer_id.is_none() {
|
||||||
|
// Throw away selections spanning multiple buffers.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) {
|
||||||
|
applicable_selections.push((
|
||||||
|
buffer,
|
||||||
|
start_position.text_anchor,
|
||||||
|
end_position.text_anchor,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if applicable_selections.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
this.linked_editing_range_task = Some(cx.spawn(|this, mut cx| async move {
|
||||||
|
let highlights = project
|
||||||
|
.update(&mut cx, |project, cx| {
|
||||||
|
let mut linked_edits_tasks = vec![];
|
||||||
|
|
||||||
|
for (buffer, start, end) in &applicable_selections {
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
let buffer_id = buffer.read(cx).remote_id();
|
||||||
|
|
||||||
|
let linked_edits_task = project.linked_edit(&buffer, *start, cx);
|
||||||
|
let highlights = move || async move {
|
||||||
|
let edits = linked_edits_task.await.log_err()?;
|
||||||
|
// Find the range containing our current selection.
|
||||||
|
// We might not find one, because the selection contains both the start and end of the contained range
|
||||||
|
// (think of selecting <`html>foo`</html> - even though there's a matching closing tag, the selection goes beyond the range of the opening tag)
|
||||||
|
// or the language server may not have returned any ranges.
|
||||||
|
|
||||||
|
let start_point = start.to_point(&snapshot);
|
||||||
|
let end_point = end.to_point(&snapshot);
|
||||||
|
let _current_selection_contains_range = edits.iter().find(|range| {
|
||||||
|
range.start.to_point(&snapshot) <= start_point
|
||||||
|
&& range.end.to_point(&snapshot) >= end_point
|
||||||
|
});
|
||||||
|
if _current_selection_contains_range.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Now link every range as each-others sibling.
|
||||||
|
let mut siblings: HashMap<Range<text::Anchor>, Vec<_>> = Default::default();
|
||||||
|
let mut insert_sorted_anchor =
|
||||||
|
|key: &Range<text::Anchor>, value: &Range<text::Anchor>| {
|
||||||
|
siblings.entry(key.clone()).or_default().push(value.clone());
|
||||||
|
};
|
||||||
|
for items in edits.into_iter().combinations(2) {
|
||||||
|
let Ok([first, second]): Result<[_; 2], _> = items.try_into() else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
insert_sorted_anchor(&first, &second);
|
||||||
|
insert_sorted_anchor(&second, &first);
|
||||||
|
}
|
||||||
|
let mut siblings: Vec<(_, _)> = siblings.into_iter().collect();
|
||||||
|
siblings.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
|
||||||
|
Some((buffer_id, siblings))
|
||||||
|
};
|
||||||
|
linked_edits_tasks.push(highlights());
|
||||||
|
}
|
||||||
|
linked_edits_tasks
|
||||||
|
})
|
||||||
|
.log_err()?;
|
||||||
|
|
||||||
|
let highlights = futures::future::join_all(highlights).await;
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.linked_edit_ranges.0.clear();
|
||||||
|
if this.pending_rename.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (buffer_id, ranges) in highlights.into_iter().flatten() {
|
||||||
|
this.linked_edit_ranges
|
||||||
|
.0
|
||||||
|
.entry(buffer_id)
|
||||||
|
.or_default()
|
||||||
|
.extend(ranges);
|
||||||
|
}
|
||||||
|
for (buffer_id, values) in this.linked_edit_ranges.0.iter_mut() {
|
||||||
|
let Some(snapshot) = this
|
||||||
|
.buffer
|
||||||
|
.read(cx)
|
||||||
|
.buffer(*buffer_id)
|
||||||
|
.map(|buffer| buffer.read(cx).snapshot())
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
values.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
|
||||||
|
Some(())
|
||||||
|
}));
|
||||||
|
None
|
||||||
|
}
|
@ -116,6 +116,8 @@ pub struct LanguageSettings {
|
|||||||
pub always_treat_brackets_as_autoclosed: bool,
|
pub always_treat_brackets_as_autoclosed: bool,
|
||||||
/// Which code actions to run on save
|
/// Which code actions to run on save
|
||||||
pub code_actions_on_format: HashMap<String, bool>,
|
pub code_actions_on_format: HashMap<String, bool>,
|
||||||
|
/// Whether to perform linked edits
|
||||||
|
pub linked_edits: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LanguageSettings {
|
impl LanguageSettings {
|
||||||
@ -326,6 +328,11 @@ pub struct LanguageSettingsContent {
|
|||||||
///
|
///
|
||||||
/// Default: {} (or {"source.organizeImports": true} for Go).
|
/// Default: {} (or {"source.organizeImports": true} for Go).
|
||||||
pub code_actions_on_format: Option<HashMap<String, bool>>,
|
pub code_actions_on_format: Option<HashMap<String, bool>>,
|
||||||
|
/// Whether to perform linked edits of associated ranges, if the language server supports it.
|
||||||
|
/// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
|
||||||
|
///
|
||||||
|
/// Default: true
|
||||||
|
pub linked_edits: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The contents of the inline completion settings.
|
/// The contents of the inline completion settings.
|
||||||
@ -785,6 +792,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
|||||||
&mut settings.code_actions_on_format,
|
&mut settings.code_actions_on_format,
|
||||||
src.code_actions_on_format.clone(),
|
src.code_actions_on_format.clone(),
|
||||||
);
|
);
|
||||||
|
merge(&mut settings.linked_edits, src.linked_edits);
|
||||||
|
|
||||||
merge(
|
merge(
|
||||||
&mut settings.preferred_line_length,
|
&mut settings.preferred_line_length,
|
||||||
|
@ -17,7 +17,7 @@ use language::{
|
|||||||
};
|
};
|
||||||
use lsp::{
|
use lsp::{
|
||||||
CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId,
|
CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId,
|
||||||
OneOf, ServerCapabilities,
|
LinkedEditingRangeServerCapabilities, OneOf, ServerCapabilities,
|
||||||
};
|
};
|
||||||
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
||||||
use text::{BufferId, LineEnding};
|
use text::{BufferId, LineEnding};
|
||||||
@ -158,6 +158,10 @@ impl From<lsp::FormattingOptions> for FormattingOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct LinkedEditingRange {
|
||||||
|
pub position: Anchor,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
impl LspCommand for PrepareRename {
|
impl LspCommand for PrepareRename {
|
||||||
type Response = Option<Range<Anchor>>;
|
type Response = Option<Range<Anchor>>;
|
||||||
@ -2559,3 +2563,150 @@ impl LspCommand for InlayHints {
|
|||||||
BufferId::new(message.buffer_id)
|
BufferId::new(message.buffer_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
impl LspCommand for LinkedEditingRange {
|
||||||
|
type Response = Vec<Range<Anchor>>;
|
||||||
|
type LspRequest = lsp::request::LinkedEditingRange;
|
||||||
|
type ProtoRequest = proto::LinkedEditingRange;
|
||||||
|
|
||||||
|
fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool {
|
||||||
|
let Some(linked_editing_options) = &server_capabilities.linked_editing_range_provider
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if let LinkedEditingRangeServerCapabilities::Simple(false) = linked_editing_options {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_lsp(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
buffer: &Buffer,
|
||||||
|
_server: &Arc<LanguageServer>,
|
||||||
|
_: &AppContext,
|
||||||
|
) -> lsp::LinkedEditingRangeParams {
|
||||||
|
let position = self.position.to_point_utf16(&buffer.snapshot());
|
||||||
|
lsp::LinkedEditingRangeParams {
|
||||||
|
text_document_position_params: lsp::TextDocumentPositionParams::new(
|
||||||
|
lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path).unwrap()),
|
||||||
|
point_to_lsp(position),
|
||||||
|
),
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn response_from_lsp(
|
||||||
|
self,
|
||||||
|
message: Option<lsp::LinkedEditingRanges>,
|
||||||
|
_project: Model<Project>,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
_server_id: LanguageServerId,
|
||||||
|
cx: AsyncAppContext,
|
||||||
|
) -> Result<Vec<Range<Anchor>>> {
|
||||||
|
if let Some(lsp::LinkedEditingRanges { mut ranges, .. }) = message {
|
||||||
|
ranges.sort_by_key(|range| range.start);
|
||||||
|
let ranges = buffer.read_with(&cx, |buffer, _| {
|
||||||
|
ranges
|
||||||
|
.into_iter()
|
||||||
|
.map(|range| {
|
||||||
|
let start =
|
||||||
|
buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left);
|
||||||
|
let end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left);
|
||||||
|
buffer.anchor_before(start)..buffer.anchor_after(end)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
ranges
|
||||||
|
} else {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LinkedEditingRange {
|
||||||
|
proto::LinkedEditingRange {
|
||||||
|
project_id,
|
||||||
|
buffer_id: buffer.remote_id().to_proto(),
|
||||||
|
position: Some(serialize_anchor(&self.position)),
|
||||||
|
version: serialize_version(&buffer.version()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn from_proto(
|
||||||
|
message: proto::LinkedEditingRange,
|
||||||
|
_project: Model<Project>,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let position = message
|
||||||
|
.position
|
||||||
|
.ok_or_else(|| anyhow!("invalid position"))?;
|
||||||
|
buffer
|
||||||
|
.update(&mut cx, |buffer, _| {
|
||||||
|
buffer.wait_for_version(deserialize_version(&message.version))
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
let position = deserialize_anchor(position).ok_or_else(|| anyhow!("invalid position"))?;
|
||||||
|
buffer
|
||||||
|
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([position]))?
|
||||||
|
.await?;
|
||||||
|
Ok(Self { position })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response_to_proto(
|
||||||
|
response: Vec<Range<Anchor>>,
|
||||||
|
_: &mut Project,
|
||||||
|
_: PeerId,
|
||||||
|
buffer_version: &clock::Global,
|
||||||
|
_: &mut AppContext,
|
||||||
|
) -> proto::LinkedEditingRangeResponse {
|
||||||
|
proto::LinkedEditingRangeResponse {
|
||||||
|
items: response
|
||||||
|
.into_iter()
|
||||||
|
.map(|range| proto::AnchorRange {
|
||||||
|
start: Some(serialize_anchor(&range.start)),
|
||||||
|
end: Some(serialize_anchor(&range.end)),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
version: serialize_version(buffer_version),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn response_from_proto(
|
||||||
|
self,
|
||||||
|
message: proto::LinkedEditingRangeResponse,
|
||||||
|
_: Model<Project>,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<Vec<Range<Anchor>>> {
|
||||||
|
buffer
|
||||||
|
.update(&mut cx, |buffer, _| {
|
||||||
|
buffer.wait_for_version(deserialize_version(&message.version))
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
let items: Vec<Range<Anchor>> = message
|
||||||
|
.items
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|range| {
|
||||||
|
let start = deserialize_anchor(range.start?)?;
|
||||||
|
let end = deserialize_anchor(range.end?)?;
|
||||||
|
Some(start..end)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for range in &items {
|
||||||
|
buffer
|
||||||
|
.update(&mut cx, |buffer, _| {
|
||||||
|
buffer.wait_for_anchors([range.start, range.end])
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_id_from_proto(message: &proto::LinkedEditingRange) -> Result<BufferId> {
|
||||||
|
BufferId::new(message.buffer_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -42,7 +42,9 @@ use gpui::{
|
|||||||
use http::{HttpClient, Url};
|
use http::{HttpClient, Url};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
|
language_settings::{
|
||||||
|
language_settings, AllLanguageSettings, FormatOnSave, Formatter, InlayHintKind,
|
||||||
|
},
|
||||||
markdown, point_to_lsp, prepare_completion_documentation,
|
markdown, point_to_lsp, prepare_completion_documentation,
|
||||||
proto::{
|
proto::{
|
||||||
deserialize_anchor, deserialize_line_ending, deserialize_version, serialize_anchor,
|
deserialize_anchor, deserialize_line_ending, deserialize_version, serialize_anchor,
|
||||||
@ -712,6 +714,7 @@ impl Project {
|
|||||||
client.add_model_request_handler(Self::handle_restart_language_servers);
|
client.add_model_request_handler(Self::handle_restart_language_servers);
|
||||||
client.add_model_request_handler(Self::handle_task_context_for_location);
|
client.add_model_request_handler(Self::handle_task_context_for_location);
|
||||||
client.add_model_request_handler(Self::handle_task_templates);
|
client.add_model_request_handler(Self::handle_task_templates);
|
||||||
|
client.add_model_request_handler(Self::handle_lsp_command::<LinkedEditingRange>);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local(
|
pub fn local(
|
||||||
@ -5804,6 +5807,62 @@ impl Project {
|
|||||||
self.hover_impl(buffer, position, cx)
|
self.hover_impl(buffer, position, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn linked_edit_impl(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
position: Anchor,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<Vec<Range<Anchor>>>> {
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
let scope = snapshot.language_scope_at(position);
|
||||||
|
let Some(server_id) = self
|
||||||
|
.language_servers_for_buffer(buffer.read(cx), cx)
|
||||||
|
.filter(|(_, server)| {
|
||||||
|
server
|
||||||
|
.capabilities()
|
||||||
|
.linked_editing_range_provider
|
||||||
|
.is_some()
|
||||||
|
})
|
||||||
|
.filter(|(adapter, _)| {
|
||||||
|
scope
|
||||||
|
.as_ref()
|
||||||
|
.map(|scope| scope.language_allowed(&adapter.name))
|
||||||
|
.unwrap_or(true)
|
||||||
|
})
|
||||||
|
.map(|(_, server)| LanguageServerToQuery::Other(server.server_id()))
|
||||||
|
.next()
|
||||||
|
.or_else(|| self.is_remote().then_some(LanguageServerToQuery::Primary))
|
||||||
|
.filter(|_| {
|
||||||
|
maybe!({
|
||||||
|
let language_name = buffer.read(cx).language_at(position)?.name();
|
||||||
|
Some(
|
||||||
|
AllLanguageSettings::get_global(cx)
|
||||||
|
.language(Some(&language_name))
|
||||||
|
.linked_edits,
|
||||||
|
)
|
||||||
|
}) == Some(true)
|
||||||
|
})
|
||||||
|
else {
|
||||||
|
return Task::ready(Ok(vec![]));
|
||||||
|
};
|
||||||
|
|
||||||
|
self.request_lsp(
|
||||||
|
buffer.clone(),
|
||||||
|
server_id,
|
||||||
|
LinkedEditingRange { position },
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn linked_edit(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
position: Anchor,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<Vec<Range<Anchor>>>> {
|
||||||
|
self.linked_edit_impl(buffer, position, cx)
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
fn completions_impl(
|
fn completions_impl(
|
||||||
&self,
|
&self,
|
||||||
|
@ -220,7 +220,7 @@ message Envelope {
|
|||||||
|
|
||||||
MultiLspQuery multi_lsp_query = 175;
|
MultiLspQuery multi_lsp_query = 175;
|
||||||
MultiLspQueryResponse multi_lsp_query_response = 176;
|
MultiLspQueryResponse multi_lsp_query_response = 176;
|
||||||
RestartLanguageServers restart_language_servers = 208; // current max
|
RestartLanguageServers restart_language_servers = 208;
|
||||||
|
|
||||||
CreateDevServerProject create_dev_server_project = 177;
|
CreateDevServerProject create_dev_server_project = 177;
|
||||||
CreateDevServerProjectResponse create_dev_server_project_response = 188;
|
CreateDevServerProjectResponse create_dev_server_project_response = 188;
|
||||||
@ -253,6 +253,9 @@ message Envelope {
|
|||||||
TaskContext task_context = 204;
|
TaskContext task_context = 204;
|
||||||
TaskTemplatesResponse task_templates_response = 205;
|
TaskTemplatesResponse task_templates_response = 205;
|
||||||
TaskTemplates task_templates = 206;
|
TaskTemplates task_templates = 206;
|
||||||
|
|
||||||
|
LinkedEditingRange linked_editing_range = 209;
|
||||||
|
LinkedEditingRangeResponse linked_editing_range_response = 210; // current max
|
||||||
}
|
}
|
||||||
|
|
||||||
reserved 158 to 161;
|
reserved 158 to 161;
|
||||||
@ -987,6 +990,24 @@ message OnTypeFormattingResponse {
|
|||||||
Transaction transaction = 1;
|
Transaction transaction = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message LinkedEditingRange {
|
||||||
|
uint64 project_id = 1;
|
||||||
|
uint64 buffer_id = 2;
|
||||||
|
Anchor position = 3;
|
||||||
|
repeated VectorClockEntry version = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AnchorRange {
|
||||||
|
Anchor start = 1;
|
||||||
|
Anchor end = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LinkedEditingRangeResponse {
|
||||||
|
repeated AnchorRange items = 1;
|
||||||
|
repeated VectorClockEntry version = 4;
|
||||||
|
}
|
||||||
|
|
||||||
message InlayHints {
|
message InlayHints {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
uint64 buffer_id = 2;
|
uint64 buffer_id = 2;
|
||||||
|
@ -336,6 +336,8 @@ messages!(
|
|||||||
(RenameDevServer, Foreground),
|
(RenameDevServer, Foreground),
|
||||||
(OpenNewBuffer, Foreground),
|
(OpenNewBuffer, Foreground),
|
||||||
(RestartLanguageServers, Foreground),
|
(RestartLanguageServers, Foreground),
|
||||||
|
(LinkedEditingRange, Background),
|
||||||
|
(LinkedEditingRangeResponse, Background)
|
||||||
);
|
);
|
||||||
|
|
||||||
request_messages!(
|
request_messages!(
|
||||||
@ -376,6 +378,7 @@ request_messages!(
|
|||||||
(GetReferences, GetReferencesResponse),
|
(GetReferences, GetReferencesResponse),
|
||||||
(GetSupermavenApiKey, GetSupermavenApiKeyResponse),
|
(GetSupermavenApiKey, GetSupermavenApiKeyResponse),
|
||||||
(GetTypeDefinition, GetTypeDefinitionResponse),
|
(GetTypeDefinition, GetTypeDefinitionResponse),
|
||||||
|
(LinkedEditingRange, LinkedEditingRangeResponse),
|
||||||
(GetUsers, UsersResponse),
|
(GetUsers, UsersResponse),
|
||||||
(IncomingCall, Ack),
|
(IncomingCall, Ack),
|
||||||
(InlayHints, InlayHintsResponse),
|
(InlayHints, InlayHintsResponse),
|
||||||
@ -475,6 +478,7 @@ entity_messages!(
|
|||||||
InlayHints,
|
InlayHints,
|
||||||
JoinProject,
|
JoinProject,
|
||||||
LeaveProject,
|
LeaveProject,
|
||||||
|
LinkedEditingRange,
|
||||||
MultiLspQuery,
|
MultiLspQuery,
|
||||||
RestartLanguageServers,
|
RestartLanguageServers,
|
||||||
OnTypeFormatting,
|
OnTypeFormatting,
|
||||||
|
Loading…
Reference in New Issue
Block a user