Skip to content

Commit 59732df

Browse files
bors[bot]matklad
andauthored
Merge #4557
4557: Formalize JoinLines protocol extension r=matklad a=matklad bors r+ 🤖 Co-authored-by: Aleksey Kladov <[email protected]>
2 parents ba6cf63 + 5b5ebec commit 59732df

File tree

9 files changed

+129
-47
lines changed

9 files changed

+129
-47
lines changed

crates/ra_ide/src/lib.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ pub use ra_ide_db::{
8989
symbol_index::Query,
9090
RootDatabase,
9191
};
92+
pub use ra_text_edit::{Indel, TextEdit};
9293

9394
pub type Cancelable<T> = Result<T, Canceled>;
9495

@@ -285,14 +286,10 @@ impl Analysis {
285286

286287
/// Returns an edit to remove all newlines in the range, cleaning up minor
287288
/// stuff like trailing commas.
288-
pub fn join_lines(&self, frange: FileRange) -> Cancelable<SourceChange> {
289+
pub fn join_lines(&self, frange: FileRange) -> Cancelable<TextEdit> {
289290
self.with_db(|db| {
290291
let parse = db.parse(frange.file_id);
291-
let file_edit = SourceFileEdit {
292-
file_id: frange.file_id,
293-
edit: join_lines::join_lines(&parse.tree(), frange.range),
294-
};
295-
SourceChange::source_file_edit("Join lines", file_edit)
292+
join_lines::join_lines(&parse.tree(), frange.range)
296293
})
297294
}
298295

crates/ra_text_edit/src/lib.rs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub struct Indel {
1717
pub delete: TextRange,
1818
}
1919

20-
#[derive(Debug, Clone)]
20+
#[derive(Default, Debug, Clone)]
2121
pub struct TextEdit {
2222
indels: Vec<Indel>,
2323
}
@@ -64,14 +64,6 @@ impl TextEdit {
6464
builder.finish()
6565
}
6666

67-
pub(crate) fn from_indels(mut indels: Vec<Indel>) -> TextEdit {
68-
indels.sort_by_key(|a| (a.delete.start(), a.delete.end()));
69-
for (a1, a2) in indels.iter().zip(indels.iter().skip(1)) {
70-
assert!(a1.delete.end() <= a2.delete.start())
71-
}
72-
TextEdit { indels }
73-
}
74-
7567
pub fn len(&self) -> usize {
7668
self.indels.len()
7769
}
@@ -122,6 +114,17 @@ impl TextEdit {
122114
*text = buf
123115
}
124116

117+
pub fn union(&mut self, other: TextEdit) -> Result<(), TextEdit> {
118+
// FIXME: can be done without allocating intermediate vector
119+
let mut all = self.iter().chain(other.iter()).collect::<Vec<_>>();
120+
if !check_disjoint(&mut all) {
121+
return Err(other);
122+
}
123+
self.indels.extend(other.indels);
124+
assert!(check_disjoint(&mut self.indels));
125+
Ok(())
126+
}
127+
125128
pub fn apply_to_offset(&self, offset: TextSize) -> Option<TextSize> {
126129
let mut res = offset;
127130
for indel in self.indels.iter() {
@@ -149,9 +152,19 @@ impl TextEditBuilder {
149152
self.indels.push(Indel::insert(offset, text))
150153
}
151154
pub fn finish(self) -> TextEdit {
152-
TextEdit::from_indels(self.indels)
155+
let mut indels = self.indels;
156+
assert!(check_disjoint(&mut indels));
157+
TextEdit { indels }
153158
}
154159
pub fn invalidates_offset(&self, offset: TextSize) -> bool {
155160
self.indels.iter().any(|indel| indel.delete.contains_inclusive(offset))
156161
}
157162
}
163+
164+
fn check_disjoint(indels: &mut [impl std::borrow::Borrow<Indel>]) -> bool {
165+
indels.sort_by_key(|indel| (indel.borrow().delete.start(), indel.borrow().delete.end()));
166+
indels
167+
.iter()
168+
.zip(indels.iter().skip(1))
169+
.all(|(l, r)| l.borrow().delete.end() <= r.borrow().delete.start())
170+
}

crates/rust-analyzer/src/caps.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
//! Advertizes the capabilities of the LSP Server.
22
use std::env;
33

4-
use crate::semantic_tokens;
5-
64
use lsp_types::{
75
CallHierarchyServerCapability, CodeActionOptions, CodeActionProviderCapability,
86
CodeLensOptions, CompletionOptions, DocumentOnTypeFormattingOptions,
@@ -12,6 +10,9 @@ use lsp_types::{
1210
ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
1311
TextDocumentSyncOptions, TypeDefinitionProviderCapability, WorkDoneProgressOptions,
1412
};
13+
use serde_json::json;
14+
15+
use crate::semantic_tokens;
1516

1617
pub fn server_capabilities() -> ServerCapabilities {
1718
ServerCapabilities {
@@ -91,6 +92,8 @@ pub fn server_capabilities() -> ServerCapabilities {
9192
}
9293
.into(),
9394
),
94-
experimental: Default::default(),
95+
experimental: Some(json!({
96+
"joinLines": true,
97+
})),
9598
}
9699
}

crates/rust-analyzer/src/lsp_ext.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,15 @@ pub enum JoinLines {}
8787

8888
impl Request for JoinLines {
8989
type Params = JoinLinesParams;
90-
type Result = SourceChange;
91-
const METHOD: &'static str = "rust-analyzer/joinLines";
90+
type Result = Vec<lsp_types::TextEdit>;
91+
const METHOD: &'static str = "experimental/joinLines";
9292
}
9393

9494
#[derive(Deserialize, Serialize, Debug)]
9595
#[serde(rename_all = "camelCase")]
9696
pub struct JoinLinesParams {
9797
pub text_document: TextDocumentIdentifier,
98-
pub range: Range,
98+
pub ranges: Vec<Range>,
9999
}
100100

101101
pub enum OnEnter {}

crates/rust-analyzer/src/main_loop/handlers.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ use lsp_types::{
1515
DocumentSymbol, FoldingRange, FoldingRangeParams, Hover, HoverContents, Location,
1616
MarkupContent, MarkupKind, Position, PrepareRenameResponse, Range, RenameParams,
1717
SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult,
18-
SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, TextEdit, Url, WorkspaceEdit,
18+
SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, Url, WorkspaceEdit,
1919
};
2020
use ra_ide::{
2121
Assist, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope,
22+
TextEdit,
2223
};
2324
use ra_prof::profile;
2425
use ra_project_model::TargetKind;
@@ -149,11 +150,24 @@ pub fn handle_find_matching_brace(
149150
pub fn handle_join_lines(
150151
world: WorldSnapshot,
151152
params: lsp_ext::JoinLinesParams,
152-
) -> Result<lsp_ext::SourceChange> {
153+
) -> Result<Vec<lsp_types::TextEdit>> {
153154
let _p = profile("handle_join_lines");
154-
let frange = from_proto::file_range(&world, params.text_document, params.range)?;
155-
let source_change = world.analysis().join_lines(frange)?;
156-
to_proto::source_change(&world, source_change)
155+
let file_id = from_proto::file_id(&world, &params.text_document.uri)?;
156+
let line_index = world.analysis().file_line_index(file_id)?;
157+
let line_endings = world.file_line_endings(file_id);
158+
let mut res = TextEdit::default();
159+
for range in params.ranges {
160+
let range = from_proto::text_range(&line_index, range);
161+
let edit = world.analysis().join_lines(FileRange { file_id, range })?;
162+
match res.union(edit) {
163+
Ok(()) => (),
164+
Err(_edit) => {
165+
// just ignore overlapping edits
166+
}
167+
}
168+
}
169+
let res = to_proto::text_edit_vec(&line_index, line_endings, res);
170+
Ok(res)
157171
}
158172

159173
pub fn handle_on_enter(
@@ -172,7 +186,7 @@ pub fn handle_on_enter(
172186
pub fn handle_on_type_formatting(
173187
world: WorldSnapshot,
174188
params: lsp_types::DocumentOnTypeFormattingParams,
175-
) -> Result<Option<Vec<TextEdit>>> {
189+
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
176190
let _p = profile("handle_on_type_formatting");
177191
let mut position = from_proto::file_position(&world, params.text_document_position)?;
178192
let line_index = world.analysis().file_line_index(position.file_id)?;
@@ -618,7 +632,7 @@ pub fn handle_references(
618632
pub fn handle_formatting(
619633
world: WorldSnapshot,
620634
params: DocumentFormattingParams,
621-
) -> Result<Option<Vec<TextEdit>>> {
635+
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
622636
let _p = profile("handle_formatting");
623637
let file_id = from_proto::file_id(&world, &params.text_document.uri)?;
624638
let file = world.analysis().file_text(file_id)?;
@@ -685,7 +699,7 @@ pub fn handle_formatting(
685699
}
686700
}
687701

688-
Ok(Some(vec![TextEdit {
702+
Ok(Some(vec![lsp_types::TextEdit {
689703
range: Range::new(Position::new(0, 0), end_position),
690704
new_text: captured_stdout,
691705
}]))

crates/rust-analyzer/src/to_proto.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
use ra_db::{FileId, FileRange};
33
use ra_ide::{
44
Assist, CompletionItem, CompletionItemKind, Documentation, FileSystemEdit, Fold, FoldKind,
5-
FunctionSignature, Highlight, HighlightModifier, HighlightTag, HighlightedRange, InlayHint,
6-
InlayKind, InsertTextFormat, LineIndex, NavigationTarget, ReferenceAccess, Severity,
7-
SourceChange, SourceFileEdit,
5+
FunctionSignature, Highlight, HighlightModifier, HighlightTag, HighlightedRange, Indel,
6+
InlayHint, InlayKind, InsertTextFormat, LineIndex, NavigationTarget, ReferenceAccess, Severity,
7+
SourceChange, SourceFileEdit, TextEdit,
88
};
99
use ra_syntax::{SyntaxKind, TextRange, TextSize};
10-
use ra_text_edit::{Indel, TextEdit};
1110
use ra_vfs::LineEndings;
1211

1312
use crate::{lsp_ext, semantic_tokens, world::WorldSnapshot, Result};

docs/dev/lsp-extensions.md

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,7 @@ All capabilities are enabled via `experimental` field of `ClientCapabilities`.
77

88
## `SnippetTextEdit`
99

10-
**Capability**
11-
12-
```typescript
13-
{
14-
"snippetTextEdit": boolean
15-
}
16-
```
10+
**Client Capability:** `{ "snippetTextEdit": boolean }`
1711

1812
If this capability is set, `WorkspaceEdit`s returned from `codeAction` requests might contain `SnippetTextEdit`s instead of usual `TextEdit`s:
1913

@@ -32,3 +26,61 @@ export interface TextDocumentEdit {
3226

3327
When applying such code action, the editor should insert snippet, with tab stops and placeholder.
3428
At the moment, rust-analyzer guarantees that only a single edit will have `InsertTextFormat.Snippet`.
29+
30+
### Example
31+
32+
"Add `derive`" code action transforms `struct S;` into `#[derive($0)] struct S;`
33+
34+
### Unresolved Questions
35+
36+
* Where exactly are `SnippetTextEdit`s allowed (only in code actions at the moment)?
37+
* Can snippets span multiple files (so far, no)?
38+
39+
## `joinLines`
40+
41+
**Server Capability:** `{ "joinLines": boolean }`
42+
43+
This request is send from client to server to handle "Join Lines" editor action.
44+
45+
**Method:** `experimental/JoinLines`
46+
47+
**Request:**
48+
49+
```typescript
50+
interface JoinLinesParams {
51+
textDocument: TextDocumentIdentifier,
52+
/// Currently active selections/cursor offsets.
53+
/// This is an array to support multiple cursors.
54+
ranges: Range[],
55+
}
56+
```
57+
58+
**Response:**
59+
60+
```typescript
61+
TextEdit[]
62+
```
63+
64+
### Example
65+
66+
```rust
67+
fn main() {
68+
/*cursor here*/let x = {
69+
92
70+
};
71+
}
72+
```
73+
74+
`experimental/joinLines` yields (curly braces are automagiacally removed)
75+
76+
```rust
77+
fn main() {
78+
let x = 92;
79+
}
80+
```
81+
82+
### Unresolved Question
83+
84+
* What is the position of the cursor after `joinLines`?
85+
Currently this is left to editor's discretion, but it might be useful to specify on the server via snippets.
86+
However, it then becomes unclear how it works with multi cursor.
Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import * as ra from '../rust-analyzer-api';
2+
import * as lc from 'vscode-languageclient';
23

34
import { Ctx, Cmd } from '../ctx';
4-
import { applySourceChange } from '../source_change';
55

66
export function joinLines(ctx: Ctx): Cmd {
77
return async () => {
88
const editor = ctx.activeRustEditor;
99
const client = ctx.client;
1010
if (!editor || !client) return;
1111

12-
const change = await client.sendRequest(ra.joinLines, {
13-
range: client.code2ProtocolConverter.asRange(editor.selection),
12+
const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
13+
ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
1414
textDocument: { uri: editor.document.uri.toString() },
1515
});
16-
await applySourceChange(ctx, change);
16+
editor.edit((builder) => {
17+
client.protocol2CodeConverter.asTextEdits(items).forEach((edit) => {
18+
builder.replace(edit.range, edit.newText);
19+
});
20+
});
1721
};
1822
}

editors/code/src/rust-analyzer-api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ export const parentModule = request<lc.TextDocumentPositionParams, Vec<lc.Locati
6464

6565
export interface JoinLinesParams {
6666
textDocument: lc.TextDocumentIdentifier;
67-
range: lc.Range;
67+
ranges: lc.Range[];
6868
}
69-
export const joinLines = request<JoinLinesParams, SourceChange>("joinLines");
69+
export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], unknown>('experimental/joinLines');
7070

7171

7272
export const onEnter = request<lc.TextDocumentPositionParams, Option<lc.WorkspaceEdit>>("onEnter");

0 commit comments

Comments
 (0)