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:
Piotr Osiewicz
2024-06-11 15:52:38 +02:00
committed by GitHub
co-authored by Bennet
parent 98659eabf1
commit b6ea393d14
9 changed files with 574 additions and 8 deletions
+152 -1
View File
@@ -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)
}
}
+60 -1
View File
@@ -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,