aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/ra_ide/src/lib.rs3
-rw-r--r--crates/ra_ide/src/typing/on_enter.rs11
-rw-r--r--crates/rust-analyzer/src/caps.rs1
-rw-r--r--crates/rust-analyzer/src/lsp_ext.rs4
-rw-r--r--crates/rust-analyzer/src/main_loop/handlers.rs14
-rw-r--r--crates/rust-analyzer/src/to_proto.rs12
-rw-r--r--crates/rust-analyzer/tests/heavy_tests/main.rs46
-rw-r--r--docs/dev/lsp-extensions.md53
-rw-r--r--editors/code/src/commands.ts10
-rw-r--r--editors/code/src/rust-analyzer-api.ts3
-rw-r--r--editors/code/src/snippets.ts3
11 files changed, 105 insertions, 55 deletions
diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs
index 5ac002d82..d983cd910 100644
--- a/crates/ra_ide/src/lib.rs
+++ b/crates/ra_ide/src/lib.rs
@@ -309,7 +309,8 @@ impl Analysis {
309 309
310 /// Returns an edit which should be applied when opening a new line, fixing 310 /// Returns an edit which should be applied when opening a new line, fixing
311 /// up minor stuff like continuing the comment. 311 /// up minor stuff like continuing the comment.
312 pub fn on_enter(&self, position: FilePosition) -> Cancelable<Option<SourceChange>> { 312 /// The edit will be a snippet (with `$0`).
313 pub fn on_enter(&self, position: FilePosition) -> Cancelable<Option<TextEdit>> {
313 self.with_db(|db| typing::on_enter(&db, position)) 314 self.with_db(|db| typing::on_enter(&db, position))
314 } 315 }
315 316
diff --git a/crates/ra_ide/src/typing/on_enter.rs b/crates/ra_ide/src/typing/on_enter.rs
index e7d64b4f6..a40d8af9c 100644
--- a/crates/ra_ide/src/typing/on_enter.rs
+++ b/crates/ra_ide/src/typing/on_enter.rs
@@ -11,9 +11,7 @@ use ra_syntax::{
11}; 11};
12use ra_text_edit::TextEdit; 12use ra_text_edit::TextEdit;
13 13
14use crate::{SourceChange, SourceFileEdit}; 14pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> {
15
16pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> {
17 let parse = db.parse(position.file_id); 15 let parse = db.parse(position.file_id);
18 let file = parse.tree(); 16 let file = parse.tree();
19 let comment = file 17 let comment = file
@@ -41,9 +39,7 @@ pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<Sour
41 let inserted = format!("\n{}{} $0", indent, prefix); 39 let inserted = format!("\n{}{} $0", indent, prefix);
42 let edit = TextEdit::insert(position.offset, inserted); 40 let edit = TextEdit::insert(position.offset, inserted);
43 41
44 let mut res = SourceChange::from(SourceFileEdit { edit, file_id: position.file_id }); 42 Some(edit)
45 res.is_snippet = true;
46 Some(res)
47} 43}
48 44
49fn followed_by_comment(comment: &ast::Comment) -> bool { 45fn followed_by_comment(comment: &ast::Comment) -> bool {
@@ -90,9 +86,8 @@ mod tests {
90 let (analysis, file_id) = single_file(&before); 86 let (analysis, file_id) = single_file(&before);
91 let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; 87 let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?;
92 88
93 assert_eq!(result.source_file_edits.len(), 1);
94 let mut actual = before.to_string(); 89 let mut actual = before.to_string();
95 result.source_file_edits[0].edit.apply(&mut actual); 90 result.apply(&mut actual);
96 Some(actual) 91 Some(actual)
97 } 92 }
98 93
diff --git a/crates/rust-analyzer/src/caps.rs b/crates/rust-analyzer/src/caps.rs
index 780fc9317..d55cbb15f 100644
--- a/crates/rust-analyzer/src/caps.rs
+++ b/crates/rust-analyzer/src/caps.rs
@@ -85,6 +85,7 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti
85 experimental: Some(json!({ 85 experimental: Some(json!({
86 "joinLines": true, 86 "joinLines": true,
87 "ssr": true, 87 "ssr": true,
88 "onEnter": true,
88 })), 89 })),
89 } 90 }
90} 91}
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 52e4fcbec..1cce1baa4 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -102,8 +102,8 @@ pub enum OnEnter {}
102 102
103impl Request for OnEnter { 103impl Request for OnEnter {
104 type Params = lsp_types::TextDocumentPositionParams; 104 type Params = lsp_types::TextDocumentPositionParams;
105 type Result = Option<SnippetWorkspaceEdit>; 105 type Result = Option<Vec<SnippetTextEdit>>;
106 const METHOD: &'static str = "rust-analyzer/onEnter"; 106 const METHOD: &'static str = "experimental/onEnter";
107} 107}
108 108
109pub enum Runnables {} 109pub enum Runnables {}
diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs
index d73107968..a13a0e1f5 100644
--- a/crates/rust-analyzer/src/main_loop/handlers.rs
+++ b/crates/rust-analyzer/src/main_loop/handlers.rs
@@ -174,13 +174,17 @@ pub fn handle_join_lines(
174pub fn handle_on_enter( 174pub fn handle_on_enter(
175 world: WorldSnapshot, 175 world: WorldSnapshot,
176 params: lsp_types::TextDocumentPositionParams, 176 params: lsp_types::TextDocumentPositionParams,
177) -> Result<Option<lsp_ext::SnippetWorkspaceEdit>> { 177) -> Result<Option<Vec<lsp_ext::SnippetTextEdit>>> {
178 let _p = profile("handle_on_enter"); 178 let _p = profile("handle_on_enter");
179 let position = from_proto::file_position(&world, params)?; 179 let position = from_proto::file_position(&world, params)?;
180 match world.analysis().on_enter(position)? { 180 let edit = match world.analysis().on_enter(position)? {
181 None => Ok(None), 181 None => return Ok(None),
182 Some(source_change) => to_proto::snippet_workspace_edit(&world, source_change).map(Some), 182 Some(it) => it,
183 } 183 };
184 let line_index = world.analysis().file_line_index(position.file_id)?;
185 let line_endings = world.file_line_endings(position.file_id);
186 let edit = to_proto::snippet_text_edit_vec(&line_index, line_endings, true, edit);
187 Ok(Some(edit))
184} 188}
185 189
186// Don't forget to add new trigger characters to `ServerCapabilities` in `caps.rs`. 190// Don't forget to add new trigger characters to `ServerCapabilities` in `caps.rs`.
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index 81a347247..39d58f1e0 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -135,6 +135,18 @@ pub(crate) fn text_edit_vec(
135 text_edit.into_iter().map(|indel| self::text_edit(line_index, line_endings, indel)).collect() 135 text_edit.into_iter().map(|indel| self::text_edit(line_index, line_endings, indel)).collect()
136} 136}
137 137
138pub(crate) fn snippet_text_edit_vec(
139 line_index: &LineIndex,
140 line_endings: LineEndings,
141 is_snippet: bool,
142 text_edit: TextEdit,
143) -> Vec<lsp_ext::SnippetTextEdit> {
144 text_edit
145 .into_iter()
146 .map(|indel| self::snippet_text_edit(line_index, line_endings, is_snippet, indel))
147 .collect()
148}
149
138pub(crate) fn completion_item( 150pub(crate) fn completion_item(
139 line_index: &LineIndex, 151 line_index: &LineIndex,
140 line_endings: LineEndings, 152 line_endings: LineEndings,
diff --git a/crates/rust-analyzer/tests/heavy_tests/main.rs b/crates/rust-analyzer/tests/heavy_tests/main.rs
index 738a9a8e3..b1bfc968a 100644
--- a/crates/rust-analyzer/tests/heavy_tests/main.rs
+++ b/crates/rust-analyzer/tests/heavy_tests/main.rs
@@ -473,23 +473,14 @@ fn main() {{}}
473 text_document: server.doc_id("src/m0.rs"), 473 text_document: server.doc_id("src/m0.rs"),
474 position: Position { line: 0, character: 5 }, 474 position: Position { line: 0, character: 5 },
475 }, 475 },
476 json!({ 476 json!([{
477 "documentChanges": [ 477 "insertTextFormat": 2,
478 { 478 "newText": "\n/// $0",
479 "edits": [ 479 "range": {
480 { 480 "end": { "character": 5, "line": 0 },
481 "insertTextFormat": 2, 481 "start": { "character": 5, "line": 0 }
482 "newText": "\n/// $0",
483 "range": {
484 "end": { "character": 5, "line": 0 },
485 "start": { "character": 5, "line": 0 }
486 }
487 }
488 ],
489 "textDocument": { "uri": "file:///[..]src/m0.rs", "version": null }
490 } 482 }
491 ] 483 }]),
492 }),
493 ); 484 );
494 let elapsed = start.elapsed(); 485 let elapsed = start.elapsed();
495 assert!(elapsed.as_millis() < 2000, "typing enter took {:?}", elapsed); 486 assert!(elapsed.as_millis() < 2000, "typing enter took {:?}", elapsed);
@@ -519,23 +510,14 @@ version = \"0.0.0\"
519 text_document: server.doc_id("src/main.rs"), 510 text_document: server.doc_id("src/main.rs"),
520 position: Position { line: 0, character: 8 }, 511 position: Position { line: 0, character: 8 },
521 }, 512 },
522 json!({ 513 json!([{
523 "documentChanges": [ 514 "insertTextFormat": 2,
524 { 515 "newText": "\r\n/// $0",
525 "edits": [ 516 "range": {
526 { 517 "end": { "line": 0, "character": 8 },
527 "insertTextFormat": 2, 518 "start": { "line": 0, "character": 8 }
528 "newText": "\r\n/// $0",
529 "range": {
530 "end": { "line": 0, "character": 8 },
531 "start": { "line": 0, "character": 8 }
532 }
533 }
534 ],
535 "textDocument": { "uri": "file:///[..]src/main.rs", "version": null }
536 } 519 }
537 ] 520 }]),
538 }),
539 ); 521 );
540} 522}
541 523
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index 55035cfae..e4b9fb2c2 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -138,6 +138,59 @@ fn main() {
138 Currently this is left to editor's discretion, but it might be useful to specify on the server via snippets. 138 Currently this is left to editor's discretion, but it might be useful to specify on the server via snippets.
139 However, it then becomes unclear how it works with multi cursor. 139 However, it then becomes unclear how it works with multi cursor.
140 140
141## On Enter
142
143**Issue:** https://github.com/microsoft/language-server-protocol/issues/1001
144
145**Server Capability:** `{ "onEnter": boolean }`
146
147This request is send from client to server to handle <kbd>Enter</kbd> keypress.
148
149**Method:** `experimental/onEnter`
150
151**Request:**: `TextDocumentPositionParams`
152
153**Response:**
154
155```typescript
156SnippetTextEdit[]
157```
158
159### Example
160
161```rust
162fn main() {
163 // Some /*cursor here*/ docs
164 let x = 92;
165}
166```
167
168`experimental/onEnter` returns the following snippet
169
170```rust
171fn main() {
172 // Some
173 // $0 docs
174 let x = 92;
175}
176```
177
178The primary goal of `onEnter` is to handle automatic indentation when opening a new line.
179This is not yet implemented.
180The secondary goal is to handle fixing up syntax, like continuing doc strings and comments, and escaping `\n` in string literals.
181
182As proper cursor positioning is raison-d'etat for `onEnter`, it uses `SnippetTextEdit`.
183
184### Unresolved Question
185
186* How to deal with synchronicity of the request?
187 One option is to require the client to block until the server returns the response.
188 Another option is to do a OT-style merging of edits from client and server.
189 A third option is to do a record-replay: client applies heuristic on enter immediatelly, then applies all user's keypresses.
190 When the server is ready with the response, the client rollbacks all the changes and applies the recorded actions on top of the correct response.
191* How to deal with multiple carets?
192* Should we extend this to arbitrary typed events and not just `onEnter`?
193
141## Structural Search Replace (SSR) 194## Structural Search Replace (SSR)
142 195
143**Server Capability:** `{ "ssr": boolean }` 196**Server Capability:** `{ "ssr": boolean }`
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 573af5aa5..e08030140 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -3,7 +3,7 @@ import * as lc from 'vscode-languageclient';
3import * as ra from './rust-analyzer-api'; 3import * as ra from './rust-analyzer-api';
4 4
5import { Ctx, Cmd } from './ctx'; 5import { Ctx, Cmd } from './ctx';
6import { applySnippetWorkspaceEdit } from './snippets'; 6import { applySnippetWorkspaceEdit, applySnippetTextEdits } from './snippets';
7import { spawnSync } from 'child_process'; 7import { spawnSync } from 'child_process';
8import { RunnableQuickPick, selectRunnable, createTask } from './run'; 8import { RunnableQuickPick, selectRunnable, createTask } from './run';
9import { AstInspector } from './ast_inspector'; 9import { AstInspector } from './ast_inspector';
@@ -102,7 +102,7 @@ export function onEnter(ctx: Ctx): Cmd {
102 102
103 if (!editor || !client) return false; 103 if (!editor || !client) return false;
104 104
105 const change = await client.sendRequest(ra.onEnter, { 105 const lcEdits = await client.sendRequest(ra.onEnter, {
106 textDocument: { uri: editor.document.uri.toString() }, 106 textDocument: { uri: editor.document.uri.toString() },
107 position: client.code2ProtocolConverter.asPosition( 107 position: client.code2ProtocolConverter.asPosition(
108 editor.selection.active, 108 editor.selection.active,
@@ -111,10 +111,10 @@ export function onEnter(ctx: Ctx): Cmd {
111 // client.logFailedRequest(OnEnterRequest.type, error); 111 // client.logFailedRequest(OnEnterRequest.type, error);
112 return null; 112 return null;
113 }); 113 });
114 if (!change) return false; 114 if (!lcEdits) return false;
115 115
116 const workspaceEdit = client.protocol2CodeConverter.asWorkspaceEdit(change); 116 const edits = client.protocol2CodeConverter.asTextEdits(lcEdits);
117 await applySnippetWorkspaceEdit(workspaceEdit); 117 await applySnippetTextEdits(editor, edits);
118 return true; 118 return true;
119 } 119 }
120 120
diff --git a/editors/code/src/rust-analyzer-api.ts b/editors/code/src/rust-analyzer-api.ts
index 900c5cd5b..c10c0fa78 100644
--- a/editors/code/src/rust-analyzer-api.ts
+++ b/editors/code/src/rust-analyzer-api.ts
@@ -67,8 +67,7 @@ export interface JoinLinesParams {
67} 67}
68export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], unknown>('experimental/joinLines'); 68export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], unknown>('experimental/joinLines');
69 69
70 70export const onEnter = new lc.RequestType<lc.TextDocumentPositionParams, lc.TextEdit[], unknown>('experimental/onEnter');
71export const onEnter = request<lc.TextDocumentPositionParams, Option<lc.WorkspaceEdit>>("onEnter");
72 71
73export interface RunnablesParams { 72export interface RunnablesParams {
74 textDocument: lc.TextDocumentIdentifier; 73 textDocument: lc.TextDocumentIdentifier;
diff --git a/editors/code/src/snippets.ts b/editors/code/src/snippets.ts
index 794530162..bcb3f2cc7 100644
--- a/editors/code/src/snippets.ts
+++ b/editors/code/src/snippets.ts
@@ -8,7 +8,10 @@ export async function applySnippetWorkspaceEdit(edit: vscode.WorkspaceEdit) {
8 8
9 const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString()); 9 const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString());
10 if (!editor) return; 10 if (!editor) return;
11 await applySnippetTextEdits(editor, edits);
12}
11 13
14export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vscode.TextEdit[]) {
12 let selection: vscode.Selection | undefined = undefined; 15 let selection: vscode.Selection | undefined = undefined;
13 let lineDelta = 0; 16 let lineDelta = 0;
14 await editor.edit((builder) => { 17 await editor.edit((builder) => {