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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user