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
parent 98659eabf1
commit b6ea393d14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 574 additions and 8 deletions

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)
}
}