diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs
index 5ac002d82f..d983cd9100 100644
--- a/crates/ra_ide/src/lib.rs
+++ b/crates/ra_ide/src/lib.rs
@@ -309,7 +309,8 @@ impl Analysis {
/// Returns an edit which should be applied when opening a new line, fixing
/// up minor stuff like continuing the comment.
- pub fn on_enter(&self, position: FilePosition) -> Cancelable> {
+ /// The edit will be a snippet (with `$0`).
+ pub fn on_enter(&self, position: FilePosition) -> Cancelable > {
self.with_db(|db| typing::on_enter(&db, position))
}
diff --git a/crates/ra_ide/src/typing/on_enter.rs b/crates/ra_ide/src/typing/on_enter.rs
index e7d64b4f68..a40d8af9c4 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::{
};
use ra_text_edit::TextEdit;
-use crate::{SourceChange, SourceFileEdit};
-
-pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option {
+pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option {
let parse = db.parse(position.file_id);
let file = parse.tree();
let comment = file
@@ -41,9 +39,7 @@ pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option bool {
@@ -90,9 +86,8 @@ mod tests {
let (analysis, file_id) = single_file(&before);
let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?;
- assert_eq!(result.source_file_edits.len(), 1);
let mut actual = before.to_string();
- result.source_file_edits[0].edit.apply(&mut actual);
+ result.apply(&mut actual);
Some(actual)
}
diff --git a/crates/rust-analyzer/src/caps.rs b/crates/rust-analyzer/src/caps.rs
index 780fc93174..d55cbb15fe 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
experimental: Some(json!({
"joinLines": true,
"ssr": true,
+ "onEnter": true,
})),
}
}
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 52e4fcbeca..1cce1baa45 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -102,8 +102,8 @@ pub enum OnEnter {}
impl Request for OnEnter {
type Params = lsp_types::TextDocumentPositionParams;
- type Result = Option;
- const METHOD: &'static str = "rust-analyzer/onEnter";
+ type Result = Option>;
+ const METHOD: &'static str = "experimental/onEnter";
}
pub enum Runnables {}
diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs
index d731079681..a13a0e1f52 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(
pub fn handle_on_enter(
world: WorldSnapshot,
params: lsp_types::TextDocumentPositionParams,
-) -> Result> {
+) -> Result >> {
let _p = profile("handle_on_enter");
let position = from_proto::file_position(&world, params)?;
- match world.analysis().on_enter(position)? {
- None => Ok(None),
- Some(source_change) => to_proto::snippet_workspace_edit(&world, source_change).map(Some),
- }
+ let edit = match world.analysis().on_enter(position)? {
+ None => return Ok(None),
+ Some(it) => it,
+ };
+ let line_index = world.analysis().file_line_index(position.file_id)?;
+ let line_endings = world.file_line_endings(position.file_id);
+ let edit = to_proto::snippet_text_edit_vec(&line_index, line_endings, true, edit);
+ Ok(Some(edit))
}
// 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 81a347247c..39d58f1e01 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(
text_edit.into_iter().map(|indel| self::text_edit(line_index, line_endings, indel)).collect()
}
+pub(crate) fn snippet_text_edit_vec(
+ line_index: &LineIndex,
+ line_endings: LineEndings,
+ is_snippet: bool,
+ text_edit: TextEdit,
+) -> Vec {
+ text_edit
+ .into_iter()
+ .map(|indel| self::snippet_text_edit(line_index, line_endings, is_snippet, indel))
+ .collect()
+}
+
pub(crate) fn completion_item(
line_index: &LineIndex,
line_endings: LineEndings,
diff --git a/crates/rust-analyzer/tests/heavy_tests/main.rs b/crates/rust-analyzer/tests/heavy_tests/main.rs
index 738a9a8e37..b1bfc968a8 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() {{}}
text_document: server.doc_id("src/m0.rs"),
position: Position { line: 0, character: 5 },
},
- json!({
- "documentChanges": [
- {
- "edits": [
- {
- "insertTextFormat": 2,
- "newText": "\n/// $0",
- "range": {
- "end": { "character": 5, "line": 0 },
- "start": { "character": 5, "line": 0 }
- }
- }
- ],
- "textDocument": { "uri": "file:///[..]src/m0.rs", "version": null }
+ json!([{
+ "insertTextFormat": 2,
+ "newText": "\n/// $0",
+ "range": {
+ "end": { "character": 5, "line": 0 },
+ "start": { "character": 5, "line": 0 }
}
- ]
- }),
+ }]),
);
let elapsed = start.elapsed();
assert!(elapsed.as_millis() < 2000, "typing enter took {:?}", elapsed);
@@ -519,23 +510,14 @@ version = \"0.0.0\"
text_document: server.doc_id("src/main.rs"),
position: Position { line: 0, character: 8 },
},
- json!({
- "documentChanges": [
- {
- "edits": [
- {
- "insertTextFormat": 2,
- "newText": "\r\n/// $0",
- "range": {
- "end": { "line": 0, "character": 8 },
- "start": { "line": 0, "character": 8 }
- }
- }
- ],
- "textDocument": { "uri": "file:///[..]src/main.rs", "version": null }
+ json!([{
+ "insertTextFormat": 2,
+ "newText": "\r\n/// $0",
+ "range": {
+ "end": { "line": 0, "character": 8 },
+ "start": { "line": 0, "character": 8 }
}
- ]
- }),
+ }]),
);
}
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index 55035cfae1..e4b9fb2c25 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -138,6 +138,59 @@ fn main() {
Currently this is left to editor's discretion, but it might be useful to specify on the server via snippets.
However, it then becomes unclear how it works with multi cursor.
+## On Enter
+
+**Issue:** https://github.com/microsoft/language-server-protocol/issues/1001
+
+**Server Capability:** `{ "onEnter": boolean }`
+
+This request is send from client to server to handle Enter keypress.
+
+**Method:** `experimental/onEnter`
+
+**Request:**: `TextDocumentPositionParams`
+
+**Response:**
+
+```typescript
+SnippetTextEdit[]
+```
+
+### Example
+
+```rust
+fn main() {
+ // Some /*cursor here*/ docs
+ let x = 92;
+}
+```
+
+`experimental/onEnter` returns the following snippet
+
+```rust
+fn main() {
+ // Some
+ // $0 docs
+ let x = 92;
+}
+```
+
+The primary goal of `onEnter` is to handle automatic indentation when opening a new line.
+This is not yet implemented.
+The secondary goal is to handle fixing up syntax, like continuing doc strings and comments, and escaping `\n` in string literals.
+
+As proper cursor positioning is raison-d'etat for `onEnter`, it uses `SnippetTextEdit`.
+
+### Unresolved Question
+
+* How to deal with synchronicity of the request?
+ One option is to require the client to block until the server returns the response.
+ Another option is to do a OT-style merging of edits from client and server.
+ A third option is to do a record-replay: client applies heuristic on enter immediatelly, then applies all user's keypresses.
+ 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.
+* How to deal with multiple carets?
+* Should we extend this to arbitrary typed events and not just `onEnter`?
+
## Structural Search Replace (SSR)
**Server Capability:** `{ "ssr": boolean }`
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 573af5aa58..e080301405 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -3,7 +3,7 @@ import * as lc from 'vscode-languageclient';
import * as ra from './rust-analyzer-api';
import { Ctx, Cmd } from './ctx';
-import { applySnippetWorkspaceEdit } from './snippets';
+import { applySnippetWorkspaceEdit, applySnippetTextEdits } from './snippets';
import { spawnSync } from 'child_process';
import { RunnableQuickPick, selectRunnable, createTask } from './run';
import { AstInspector } from './ast_inspector';
@@ -102,7 +102,7 @@ export function onEnter(ctx: Ctx): Cmd {
if (!editor || !client) return false;
- const change = await client.sendRequest(ra.onEnter, {
+ const lcEdits = await client.sendRequest(ra.onEnter, {
textDocument: { uri: editor.document.uri.toString() },
position: client.code2ProtocolConverter.asPosition(
editor.selection.active,
@@ -111,10 +111,10 @@ export function onEnter(ctx: Ctx): Cmd {
// client.logFailedRequest(OnEnterRequest.type, error);
return null;
});
- if (!change) return false;
+ if (!lcEdits) return false;
- const workspaceEdit = client.protocol2CodeConverter.asWorkspaceEdit(change);
- await applySnippetWorkspaceEdit(workspaceEdit);
+ const edits = client.protocol2CodeConverter.asTextEdits(lcEdits);
+ await applySnippetTextEdits(editor, edits);
return true;
}
diff --git a/editors/code/src/rust-analyzer-api.ts b/editors/code/src/rust-analyzer-api.ts
index 900c5cd5bc..c10c0fa789 100644
--- a/editors/code/src/rust-analyzer-api.ts
+++ b/editors/code/src/rust-analyzer-api.ts
@@ -67,8 +67,7 @@ export interface JoinLinesParams {
}
export const joinLines = new lc.RequestType('experimental/joinLines');
-
-export const onEnter = request>("onEnter");
+export const onEnter = new lc.RequestType('experimental/onEnter');
export interface RunnablesParams {
textDocument: lc.TextDocumentIdentifier;
diff --git a/editors/code/src/snippets.ts b/editors/code/src/snippets.ts
index 794530162d..bcb3f2cc76 100644
--- a/editors/code/src/snippets.ts
+++ b/editors/code/src/snippets.ts
@@ -8,7 +8,10 @@ export async function applySnippetWorkspaceEdit(edit: vscode.WorkspaceEdit) {
const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString());
if (!editor) return;
+ await applySnippetTextEdits(editor, edits);
+}
+export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vscode.TextEdit[]) {
let selection: vscode.Selection | undefined = undefined;
let lineDelta = 0;
await editor.edit((builder) => {