diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-05-25 13:29:47 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2020-05-25 13:29:47 +0100 |
commit | 8686d0b0ac765c2144b22b897de1d8fda68ecc6e (patch) | |
tree | 8cf69f49502a9f2b08d2d2975be79f54f9a04ccb | |
parent | e4f91bfa578e57c1ef4be3343ebb4e8950e5dae6 (diff) | |
parent | 76e170c3d0d0784c0e612c5849798c65a2034f29 (diff) |
Merge #4607
4607: Less rust-analyzer specific onEnter r=matklad a=matklad
bors r+
🤖
Co-authored-by: Aleksey Kladov <[email protected]>
-rw-r--r-- | crates/ra_ide/src/lib.rs | 3 | ||||
-rw-r--r-- | crates/ra_ide/src/typing/on_enter.rs | 11 | ||||
-rw-r--r-- | crates/rust-analyzer/src/caps.rs | 1 | ||||
-rw-r--r-- | crates/rust-analyzer/src/lsp_ext.rs | 4 | ||||
-rw-r--r-- | crates/rust-analyzer/src/main_loop/handlers.rs | 14 | ||||
-rw-r--r-- | crates/rust-analyzer/src/to_proto.rs | 12 | ||||
-rw-r--r-- | crates/rust-analyzer/tests/heavy_tests/main.rs | 46 | ||||
-rw-r--r-- | docs/dev/lsp-extensions.md | 53 | ||||
-rw-r--r-- | editors/code/src/commands.ts | 10 | ||||
-rw-r--r-- | editors/code/src/rust-analyzer-api.ts | 3 | ||||
-rw-r--r-- | editors/code/src/snippets.ts | 3 |
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 | }; |
12 | use ra_text_edit::TextEdit; | 12 | use ra_text_edit::TextEdit; |
13 | 13 | ||
14 | use crate::{SourceChange, SourceFileEdit}; | 14 | pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> { |
15 | |||
16 | pub(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 | ||
49 | fn followed_by_comment(comment: &ast::Comment) -> bool { | 45 | fn 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 | ||
103 | impl Request for OnEnter { | 103 | impl 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 | ||
109 | pub enum Runnables {} | 109 | pub 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( | |||
174 | pub fn handle_on_enter( | 174 | pub 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 | ||
138 | pub(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 | |||
138 | pub(crate) fn completion_item( | 150 | pub(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 | |||
147 | This 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 | ||
156 | SnippetTextEdit[] | ||
157 | ``` | ||
158 | |||
159 | ### Example | ||
160 | |||
161 | ```rust | ||
162 | fn main() { | ||
163 | // Some /*cursor here*/ docs | ||
164 | let x = 92; | ||
165 | } | ||
166 | ``` | ||
167 | |||
168 | `experimental/onEnter` returns the following snippet | ||
169 | |||
170 | ```rust | ||
171 | fn main() { | ||
172 | // Some | ||
173 | // $0 docs | ||
174 | let x = 92; | ||
175 | } | ||
176 | ``` | ||
177 | |||
178 | The primary goal of `onEnter` is to handle automatic indentation when opening a new line. | ||
179 | This is not yet implemented. | ||
180 | The secondary goal is to handle fixing up syntax, like continuing doc strings and comments, and escaping `\n` in string literals. | ||
181 | |||
182 | As 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'; | |||
3 | import * as ra from './rust-analyzer-api'; | 3 | import * as ra from './rust-analyzer-api'; |
4 | 4 | ||
5 | import { Ctx, Cmd } from './ctx'; | 5 | import { Ctx, Cmd } from './ctx'; |
6 | import { applySnippetWorkspaceEdit } from './snippets'; | 6 | import { applySnippetWorkspaceEdit, applySnippetTextEdits } from './snippets'; |
7 | import { spawnSync } from 'child_process'; | 7 | import { spawnSync } from 'child_process'; |
8 | import { RunnableQuickPick, selectRunnable, createTask } from './run'; | 8 | import { RunnableQuickPick, selectRunnable, createTask } from './run'; |
9 | import { AstInspector } from './ast_inspector'; | 9 | import { 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 | } |
68 | export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], unknown>('experimental/joinLines'); | 68 | export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], unknown>('experimental/joinLines'); |
69 | 69 | ||
70 | 70 | export const onEnter = new lc.RequestType<lc.TextDocumentPositionParams, lc.TextEdit[], unknown>('experimental/onEnter'); | |
71 | export const onEnter = request<lc.TextDocumentPositionParams, Option<lc.WorkspaceEdit>>("onEnter"); | ||
72 | 71 | ||
73 | export interface RunnablesParams { | 72 | export 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 | ||
14 | export 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) => { |