aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2020-05-21 18:50:23 +0100
committerAleksey Kladov <[email protected]>2020-05-21 19:05:33 +0100
commit5b5ebec440841ee98a0aa70b71a135d94f5ca077 (patch)
tree5accb5fce10496334b49ed5a823d321572b375b4
parentba6cf638fbf3d0a025e804f2d354d91abc8afd28 (diff)
Formalize JoinLines protocol extension
-rw-r--r--crates/ra_ide/src/lib.rs9
-rw-r--r--crates/ra_text_edit/src/lib.rs33
-rw-r--r--crates/rust-analyzer/src/caps.rs9
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs6
-rw-r--r--crates/rust-analyzer/src/main_loop/handlers.rs30
-rw-r--r--crates/rust-analyzer/src/to_proto.rs7
-rw-r--r--docs/dev/lsp-extensions.md66
-rw-r--r--editors/code/src/commands/join_lines.ts12
-rw-r--r--editors/code/src/rust-analyzer-api.ts4
9 files changed, 129 insertions, 47 deletions
diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs
index d0aeb3ba7..97ff67ee8 100644
--- a/crates/ra_ide/src/lib.rs
+++ b/crates/ra_ide/src/lib.rs
@@ -89,6 +89,7 @@ pub use ra_ide_db::{
89 symbol_index::Query, 89 symbol_index::Query,
90 RootDatabase, 90 RootDatabase,
91}; 91};
92pub use ra_text_edit::{Indel, TextEdit};
92 93
93pub type Cancelable<T> = Result<T, Canceled>; 94pub type Cancelable<T> = Result<T, Canceled>;
94 95
@@ -285,14 +286,10 @@ impl Analysis {
285 286
286 /// Returns an edit to remove all newlines in the range, cleaning up minor 287 /// Returns an edit to remove all newlines in the range, cleaning up minor
287 /// stuff like trailing commas. 288 /// stuff like trailing commas.
288 pub fn join_lines(&self, frange: FileRange) -> Cancelable<SourceChange> { 289 pub fn join_lines(&self, frange: FileRange) -> Cancelable<TextEdit> {
289 self.with_db(|db| { 290 self.with_db(|db| {
290 let parse = db.parse(frange.file_id); 291 let parse = db.parse(frange.file_id);
291 let file_edit = SourceFileEdit { 292 join_lines::join_lines(&parse.tree(), frange.range)
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)
296 }) 293 })
297 } 294 }
298 295
diff --git a/crates/ra_text_edit/src/lib.rs b/crates/ra_text_edit/src/lib.rs
index 199fd1096..25554f583 100644
--- a/crates/ra_text_edit/src/lib.rs
+++ b/crates/ra_text_edit/src/lib.rs
@@ -17,7 +17,7 @@ pub struct Indel {
17 pub delete: TextRange, 17 pub delete: TextRange,
18} 18}
19 19
20#[derive(Debug, Clone)] 20#[derive(Default, Debug, Clone)]
21pub struct TextEdit { 21pub struct TextEdit {
22 indels: Vec<Indel>, 22 indels: Vec<Indel>,
23} 23}
@@ -64,14 +64,6 @@ impl TextEdit {
64 builder.finish() 64 builder.finish()
65 } 65 }
66 66
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
75 pub fn len(&self) -> usize { 67 pub fn len(&self) -> usize {
76 self.indels.len() 68 self.indels.len()
77 } 69 }
@@ -122,6 +114,17 @@ impl TextEdit {
122 *text = buf 114 *text = buf
123 } 115 }
124 116
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
125 pub fn apply_to_offset(&self, offset: TextSize) -> Option<TextSize> { 128 pub fn apply_to_offset(&self, offset: TextSize) -> Option<TextSize> {
126 let mut res = offset; 129 let mut res = offset;
127 for indel in self.indels.iter() { 130 for indel in self.indels.iter() {
@@ -149,9 +152,19 @@ impl TextEditBuilder {
149 self.indels.push(Indel::insert(offset, text)) 152 self.indels.push(Indel::insert(offset, text))
150 } 153 }
151 pub fn finish(self) -> TextEdit { 154 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 }
153 } 158 }
154 pub fn invalidates_offset(&self, offset: TextSize) -> bool { 159 pub fn invalidates_offset(&self, offset: TextSize) -> bool {
155 self.indels.iter().any(|indel| indel.delete.contains_inclusive(offset)) 160 self.indels.iter().any(|indel| indel.delete.contains_inclusive(offset))
156 } 161 }
157} 162}
163
164fn 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}
diff --git a/crates/rust-analyzer/src/caps.rs b/crates/rust-analyzer/src/caps.rs
index 110c9a442..4c417c270 100644
--- a/crates/rust-analyzer/src/caps.rs
+++ b/crates/rust-analyzer/src/caps.rs
@@ -1,8 +1,6 @@
1//! Advertizes the capabilities of the LSP Server. 1//! Advertizes the capabilities of the LSP Server.
2use std::env; 2use std::env;
3 3
4use crate::semantic_tokens;
5
6use lsp_types::{ 4use lsp_types::{
7 CallHierarchyServerCapability, CodeActionOptions, CodeActionProviderCapability, 5 CallHierarchyServerCapability, CodeActionOptions, CodeActionProviderCapability,
8 CodeLensOptions, CompletionOptions, DocumentOnTypeFormattingOptions, 6 CodeLensOptions, CompletionOptions, DocumentOnTypeFormattingOptions,
@@ -12,6 +10,9 @@ use lsp_types::{
12 ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind, 10 ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
13 TextDocumentSyncOptions, TypeDefinitionProviderCapability, WorkDoneProgressOptions, 11 TextDocumentSyncOptions, TypeDefinitionProviderCapability, WorkDoneProgressOptions,
14}; 12};
13use serde_json::json;
14
15use crate::semantic_tokens;
15 16
16pub fn server_capabilities() -> ServerCapabilities { 17pub fn server_capabilities() -> ServerCapabilities {
17 ServerCapabilities { 18 ServerCapabilities {
@@ -91,6 +92,8 @@ pub fn server_capabilities() -> ServerCapabilities {
91 } 92 }
92 .into(), 93 .into(),
93 ), 94 ),
94 experimental: Default::default(), 95 experimental: Some(json!({
96 "joinLines": true,
97 })),
95 } 98 }
96} 99}
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 3c7bd609d..1bb1b02ab 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -87,15 +87,15 @@ pub enum JoinLines {}
87 87
88impl Request for JoinLines { 88impl Request for JoinLines {
89 type Params = JoinLinesParams; 89 type Params = JoinLinesParams;
90 type Result = SourceChange; 90 type Result = Vec<lsp_types::TextEdit>;
91 const METHOD: &'static str = "rust-analyzer/joinLines"; 91 const METHOD: &'static str = "experimental/joinLines";
92} 92}
93 93
94#[derive(Deserialize, Serialize, Debug)] 94#[derive(Deserialize, Serialize, Debug)]
95#[serde(rename_all = "camelCase")] 95#[serde(rename_all = "camelCase")]
96pub struct JoinLinesParams { 96pub struct JoinLinesParams {
97 pub text_document: TextDocumentIdentifier, 97 pub text_document: TextDocumentIdentifier,
98 pub range: Range, 98 pub ranges: Vec<Range>,
99} 99}
100 100
101pub enum OnEnter {} 101pub enum OnEnter {}
diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs
index fcf08cd79..121964718 100644
--- a/crates/rust-analyzer/src/main_loop/handlers.rs
+++ b/crates/rust-analyzer/src/main_loop/handlers.rs
@@ -15,10 +15,11 @@ use lsp_types::{
15 DocumentSymbol, FoldingRange, FoldingRangeParams, Hover, HoverContents, Location, 15 DocumentSymbol, FoldingRange, FoldingRangeParams, Hover, HoverContents, Location,
16 MarkupContent, MarkupKind, Position, PrepareRenameResponse, Range, RenameParams, 16 MarkupContent, MarkupKind, Position, PrepareRenameResponse, Range, RenameParams,
17 SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult, 17 SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult,
18 SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, TextEdit, Url, WorkspaceEdit, 18 SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, Url, WorkspaceEdit,
19}; 19};
20use ra_ide::{ 20use ra_ide::{
21 Assist, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope, 21 Assist, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope,
22 TextEdit,
22}; 23};
23use ra_prof::profile; 24use ra_prof::profile;
24use ra_project_model::TargetKind; 25use ra_project_model::TargetKind;
@@ -149,11 +150,24 @@ pub fn handle_find_matching_brace(
149pub fn handle_join_lines( 150pub fn handle_join_lines(
150 world: WorldSnapshot, 151 world: WorldSnapshot,
151 params: lsp_ext::JoinLinesParams, 152 params: lsp_ext::JoinLinesParams,
152) -> Result<lsp_ext::SourceChange> { 153) -> Result<Vec<lsp_types::TextEdit>> {
153 let _p = profile("handle_join_lines"); 154 let _p = profile("handle_join_lines");
154 let frange = from_proto::file_range(&world, params.text_document, params.range)?; 155 let file_id = from_proto::file_id(&world, &params.text_document.uri)?;
155 let source_change = world.analysis().join_lines(frange)?; 156 let line_index = world.analysis().file_line_index(file_id)?;
156 to_proto::source_change(&world, source_change) 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)
157} 171}
158 172
159pub fn handle_on_enter( 173pub fn handle_on_enter(
@@ -172,7 +186,7 @@ pub fn handle_on_enter(
172pub fn handle_on_type_formatting( 186pub fn handle_on_type_formatting(
173 world: WorldSnapshot, 187 world: WorldSnapshot,
174 params: lsp_types::DocumentOnTypeFormattingParams, 188 params: lsp_types::DocumentOnTypeFormattingParams,
175) -> Result<Option<Vec<TextEdit>>> { 189) -> Result<Option<Vec<lsp_types::TextEdit>>> {
176 let _p = profile("handle_on_type_formatting"); 190 let _p = profile("handle_on_type_formatting");
177 let mut position = from_proto::file_position(&world, params.text_document_position)?; 191 let mut position = from_proto::file_position(&world, params.text_document_position)?;
178 let line_index = world.analysis().file_line_index(position.file_id)?; 192 let line_index = world.analysis().file_line_index(position.file_id)?;
@@ -618,7 +632,7 @@ pub fn handle_references(
618pub fn handle_formatting( 632pub fn handle_formatting(
619 world: WorldSnapshot, 633 world: WorldSnapshot,
620 params: DocumentFormattingParams, 634 params: DocumentFormattingParams,
621) -> Result<Option<Vec<TextEdit>>> { 635) -> Result<Option<Vec<lsp_types::TextEdit>>> {
622 let _p = profile("handle_formatting"); 636 let _p = profile("handle_formatting");
623 let file_id = from_proto::file_id(&world, &params.text_document.uri)?; 637 let file_id = from_proto::file_id(&world, &params.text_document.uri)?;
624 let file = world.analysis().file_text(file_id)?; 638 let file = world.analysis().file_text(file_id)?;
@@ -685,7 +699,7 @@ pub fn handle_formatting(
685 } 699 }
686 } 700 }
687 701
688 Ok(Some(vec![TextEdit { 702 Ok(Some(vec![lsp_types::TextEdit {
689 range: Range::new(Position::new(0, 0), end_position), 703 range: Range::new(Position::new(0, 0), end_position),
690 new_text: captured_stdout, 704 new_text: captured_stdout,
691 }])) 705 }]))
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index 617197963..f6f4bb134 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -2,12 +2,11 @@
2use ra_db::{FileId, FileRange}; 2use ra_db::{FileId, FileRange};
3use ra_ide::{ 3use ra_ide::{
4 Assist, CompletionItem, CompletionItemKind, Documentation, FileSystemEdit, Fold, FoldKind, 4 Assist, CompletionItem, CompletionItemKind, Documentation, FileSystemEdit, Fold, FoldKind,
5 FunctionSignature, Highlight, HighlightModifier, HighlightTag, HighlightedRange, InlayHint, 5 FunctionSignature, Highlight, HighlightModifier, HighlightTag, HighlightedRange, Indel,
6 InlayKind, InsertTextFormat, LineIndex, NavigationTarget, ReferenceAccess, Severity, 6 InlayHint, InlayKind, InsertTextFormat, LineIndex, NavigationTarget, ReferenceAccess, Severity,
7 SourceChange, SourceFileEdit, 7 SourceChange, SourceFileEdit, TextEdit,
8}; 8};
9use ra_syntax::{SyntaxKind, TextRange, TextSize}; 9use ra_syntax::{SyntaxKind, TextRange, TextSize};
10use ra_text_edit::{Indel, TextEdit};
11use ra_vfs::LineEndings; 10use ra_vfs::LineEndings;
12 11
13use crate::{lsp_ext, semantic_tokens, world::WorldSnapshot, Result}; 12use crate::{lsp_ext, semantic_tokens, world::WorldSnapshot, Result};
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index d2ec6c021..0e3a0af1c 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -7,13 +7,7 @@ All capabilities are enabled via `experimental` field of `ClientCapabilities`.
7 7
8## `SnippetTextEdit` 8## `SnippetTextEdit`
9 9
10**Capability** 10**Client Capability:** `{ "snippetTextEdit": boolean }`
11
12```typescript
13{
14 "snippetTextEdit": boolean
15}
16```
17 11
18If this capability is set, `WorkspaceEdit`s returned from `codeAction` requests might contain `SnippetTextEdit`s instead of usual `TextEdit`s: 12If this capability is set, `WorkspaceEdit`s returned from `codeAction` requests might contain `SnippetTextEdit`s instead of usual `TextEdit`s:
19 13
@@ -32,3 +26,61 @@ export interface TextDocumentEdit {
32 26
33When applying such code action, the editor should insert snippet, with tab stops and placeholder. 27When applying such code action, the editor should insert snippet, with tab stops and placeholder.
34At the moment, rust-analyzer guarantees that only a single edit will have `InsertTextFormat.Snippet`. 28At 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
43This request is send from client to server to handle "Join Lines" editor action.
44
45**Method:** `experimental/JoinLines`
46
47**Request:**
48
49```typescript
50interface 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
61TextEdit[]
62```
63
64### Example
65
66```rust
67fn main() {
68 /*cursor here*/let x = {
69 92
70 };
71}
72```
73
74`experimental/joinLines` yields (curly braces are automagiacally removed)
75
76```rust
77fn 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.
diff --git a/editors/code/src/commands/join_lines.ts b/editors/code/src/commands/join_lines.ts
index de0614653..0bf1ee6e6 100644
--- a/editors/code/src/commands/join_lines.ts
+++ b/editors/code/src/commands/join_lines.ts
@@ -1,7 +1,7 @@
1import * as ra from '../rust-analyzer-api'; 1import * as ra from '../rust-analyzer-api';
2import * as lc from 'vscode-languageclient';
2 3
3import { Ctx, Cmd } from '../ctx'; 4import { Ctx, Cmd } from '../ctx';
4import { applySourceChange } from '../source_change';
5 5
6export function joinLines(ctx: Ctx): Cmd { 6export function joinLines(ctx: Ctx): Cmd {
7 return async () => { 7 return async () => {
@@ -9,10 +9,14 @@ export function joinLines(ctx: Ctx): Cmd {
9 const client = ctx.client; 9 const client = ctx.client;
10 if (!editor || !client) return; 10 if (!editor || !client) return;
11 11
12 const change = await client.sendRequest(ra.joinLines, { 12 const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
13 range: client.code2ProtocolConverter.asRange(editor.selection), 13 ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
14 textDocument: { uri: editor.document.uri.toString() }, 14 textDocument: { uri: editor.document.uri.toString() },
15 }); 15 });
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 });
17 }; 21 };
18} 22}
diff --git a/editors/code/src/rust-analyzer-api.ts b/editors/code/src/rust-analyzer-api.ts
index 3b83b10e3..8ed56c173 100644
--- a/editors/code/src/rust-analyzer-api.ts
+++ b/editors/code/src/rust-analyzer-api.ts
@@ -64,9 +64,9 @@ export const parentModule = request<lc.TextDocumentPositionParams, Vec<lc.Locati
64 64
65export interface JoinLinesParams { 65export interface JoinLinesParams {
66 textDocument: lc.TextDocumentIdentifier; 66 textDocument: lc.TextDocumentIdentifier;
67 range: lc.Range; 67 ranges: lc.Range[];
68} 68}
69export const joinLines = request<JoinLinesParams, SourceChange>("joinLines"); 69export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], unknown>('experimental/joinLines');
70 70
71 71
72export const onEnter = request<lc.TextDocumentPositionParams, Option<lc.WorkspaceEdit>>("onEnter"); 72export const onEnter = request<lc.TextDocumentPositionParams, Option<lc.WorkspaceEdit>>("onEnter");