Detailed changes
@@ -131,7 +131,14 @@
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 3,
// 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
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
@@ -354,6 +361,9 @@
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
"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.
//
// This is typically customized on a per-language basis.
@@ -548,6 +548,9 @@ impl Server {
.add_request_handler(user_handler(
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_request_handler(update_buffer)
.add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)
@@ -28,6 +28,7 @@ mod indent_guides;
mod inlay_hint_cache;
mod inline_completion_provider;
pub mod items;
+mod linked_editing_ranges;
mod mouse_context_menu;
pub mod movement;
mod persistence;
@@ -88,6 +89,7 @@ use language::{
Point, Selection, SelectionGoal, TransactionId,
};
use language::{BufferRow, Runnable, RunnableRange};
+use linked_editing_ranges::refresh_linked_ranges;
use task::{ResolvedTask, TaskTemplate, TaskVariables};
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
@@ -478,6 +480,8 @@ pub struct Editor {
available_code_actions: Option<(Location, Arc<[CodeAction]>)>,
code_actions_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>,
searchable: bool,
cursor_shape: CursorShape,
@@ -1768,6 +1772,7 @@ impl Editor {
available_code_actions: Default::default(),
code_actions_task: Default::default(),
document_highlights_task: Default::default(),
+ linked_editing_range_task: Default::default(),
pending_rename: Default::default(),
searchable: true,
cursor_shape: Default::default(),
@@ -1828,6 +1833,7 @@ impl Editor {
}),
],
tasks_update_task: None,
+ linked_edit_ranges: Default::default(),
previous_search_ranges: None,
};
this.tasks_update_task = Some(this.refresh_runnables(cx));
@@ -2208,7 +2214,6 @@ impl Editor {
)
});
}
-
let display_map = self
.display_map
.update(cx, |display_map, cx| display_map.snapshot(cx));
@@ -2296,6 +2301,7 @@ impl Editor {
self.refresh_document_highlights(cx);
refresh_matching_bracket_highlights(self, cx);
self.discard_inline_completion(false, cx);
+ linked_editing_ranges::refresh_linked_ranges(self, cx);
if self.git_blame_inline_enabled {
self.start_inline_blame_timer(cx);
}
@@ -2307,7 +2313,6 @@ impl Editor {
if self.selections.disjoint_anchors().len() == 1 {
cx.emit(SearchEvent::ActiveMatchChanged)
}
-
cx.notify();
}
@@ -2777,6 +2782,49 @@ impl Editor {
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>) {
let text: Arc<str> = text.into();
@@ -2787,6 +2835,7 @@ impl Editor {
let selections = self.selections.all_adjusted(cx);
let mut brace_inserted = false;
let mut edits = Vec::new();
+ let mut linked_edits = HashMap::<_, Vec<_>>::default();
let mut new_selections = Vec::with_capacity(selections.len());
let mut new_autoclose_regions = Vec::new();
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
// newly inserted text.
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));
edits.push((selection.start..selection.end, text.clone()));
}
drop(snapshot);
+
self.transact(cx, |this, cx| {
this.buffer.update(cx, |buffer, 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_selection_deltas = new_selections.iter().map(|e| e.1);
let snapshot = this.buffer.read(cx).read(cx);
@@ -3033,6 +3112,7 @@ impl Editor {
let trigger_in_words = !had_active_inline_completion;
this.trigger_completion_on_input(&text, trigger_in_words, cx);
+ linked_editing_ranges::refresh_linked_ranges(this, cx);
this.refresh_inline_completion(true, cx);
});
}
@@ -3970,6 +4050,7 @@ impl Editor {
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut range_to_replace: Option<Range<isize>> = None;
let mut ranges = Vec::new();
+ let mut linked_edits = HashMap::<_, Vec<_>>::default();
for selection in &selections {
if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) {
let start = selection.start.saturating_sub(lookbehind);
@@ -3999,6 +4080,21 @@ impl Editor {
}));
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..];
@@ -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);
});
@@ -5009,6 +5121,27 @@ impl Editor {
pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, 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);
if !this.selections.line_mode {
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.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);
+ linked_editing_ranges::refresh_linked_ranges(this, cx);
});
}
@@ -10604,7 +10763,6 @@ impl Editor {
}
cx.emit(EditorEvent::BufferEdited);
cx.emit(SearchEvent::MatchesInvalidated);
-
if *singleton_buffer_edited {
if let Some(project) = &self.project {
let project = project.read(cx);
@@ -10636,6 +10794,7 @@ impl Editor {
let Some(project) = &self.project else { return };
let telemetry = project.read(cx).client().telemetry().clone();
+ refresh_linked_ranges(self, cx);
telemetry.log_edit_event("editor");
}
multi_buffer::Event::ExcerptsAdded {
@@ -10661,6 +10820,7 @@ impl Editor {
cx.emit(EditorEvent::Reparsed);
}
multi_buffer::Event::LanguageChanged => {
+ linked_editing_ranges::refresh_linked_ranges(self, cx);
cx.emit(EditorEvent::Reparsed);
cx.notify();
}
@@ -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,
/// Which code actions to run on save
pub code_actions_on_format: HashMap<String, bool>,
+ /// Whether to perform linked edits
+ pub linked_edits: bool,
}
impl LanguageSettings {
@@ -326,6 +328,11 @@ pub struct LanguageSettingsContent {
///
/// Default: {} (or {"source.organizeImports": true} for Go).
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.
@@ -785,6 +792,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
&mut settings.code_actions_on_format,
src.code_actions_on_format.clone(),
);
+ merge(&mut settings.linked_edits, src.linked_edits);
merge(
&mut settings.preferred_line_length,
@@ -17,7 +17,7 @@ use language::{
};
use lsp::{
CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId,
- OneOf, ServerCapabilities,
+ LinkedEditingRangeServerCapabilities, OneOf, ServerCapabilities,
};
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
use text::{BufferId, LineEnding};
@@ -158,6 +158,10 @@ impl From<lsp::FormattingOptions> for FormattingOptions {
}
}
+pub(crate) struct LinkedEditingRange {
+ pub position: Anchor,
+}
+
#[async_trait(?Send)]
impl LspCommand for PrepareRename {
type Response = Option<Range<Anchor>>;
@@ -2559,3 +2563,150 @@ impl LspCommand for InlayHints {
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 itertools::Itertools;
use language::{
- language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
+ language_settings::{
+ language_settings, AllLanguageSettings, FormatOnSave, Formatter, InlayHintKind,
+ },
markdown, point_to_lsp, prepare_completion_documentation,
proto::{
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_task_context_for_location);
client.add_model_request_handler(Self::handle_task_templates);
+ client.add_model_request_handler(Self::handle_lsp_command::<LinkedEditingRange>);
}
pub fn local(
@@ -5804,6 +5807,62 @@ impl Project {
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)]
fn completions_impl(
&self,
@@ -220,7 +220,7 @@ message Envelope {
MultiLspQuery multi_lsp_query = 175;
MultiLspQueryResponse multi_lsp_query_response = 176;
- RestartLanguageServers restart_language_servers = 208; // current max
+ RestartLanguageServers restart_language_servers = 208;
CreateDevServerProject create_dev_server_project = 177;
CreateDevServerProjectResponse create_dev_server_project_response = 188;
@@ -253,6 +253,9 @@ message Envelope {
TaskContext task_context = 204;
TaskTemplatesResponse task_templates_response = 205;
TaskTemplates task_templates = 206;
+
+ LinkedEditingRange linked_editing_range = 209;
+ LinkedEditingRangeResponse linked_editing_range_response = 210; // current max
}
reserved 158 to 161;
@@ -987,6 +990,24 @@ message OnTypeFormattingResponse {
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 {
uint64 project_id = 1;
uint64 buffer_id = 2;
@@ -336,6 +336,8 @@ messages!(
(RenameDevServer, Foreground),
(OpenNewBuffer, Foreground),
(RestartLanguageServers, Foreground),
+ (LinkedEditingRange, Background),
+ (LinkedEditingRangeResponse, Background)
);
request_messages!(
@@ -376,6 +378,7 @@ request_messages!(
(GetReferences, GetReferencesResponse),
(GetSupermavenApiKey, GetSupermavenApiKeyResponse),
(GetTypeDefinition, GetTypeDefinitionResponse),
+ (LinkedEditingRange, LinkedEditingRangeResponse),
(GetUsers, UsersResponse),
(IncomingCall, Ack),
(InlayHints, InlayHintsResponse),
@@ -475,6 +478,7 @@ entity_messages!(
InlayHints,
JoinProject,
LeaveProject,
+ LinkedEditingRange,
MultiLspQuery,
RestartLanguageServers,
OnTypeFormatting,