4607: Less rust-analyzer specific onEnter r=matklad a=matklad



bors r+
🤖

Co-authored-by: Aleksey Kladov <aleksey.kladov@gmail.com>
This commit is contained in:
bors[bot] 2020-05-25 12:29:47 +00:00 committed by GitHub
commit 8686d0b0ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 105 additions and 55 deletions

View file

@ -309,7 +309,8 @@ impl Analysis {
/// Returns an edit which should be applied when opening a new line, fixing /// Returns an edit which should be applied when opening a new line, fixing
/// up minor stuff like continuing the comment. /// up minor stuff like continuing the comment.
pub fn on_enter(&self, position: FilePosition) -> Cancelable<Option<SourceChange>> { /// The edit will be a snippet (with `$0`).
pub fn on_enter(&self, position: FilePosition) -> Cancelable<Option<TextEdit>> {
self.with_db(|db| typing::on_enter(&db, position)) self.with_db(|db| typing::on_enter(&db, position))
} }

View file

@ -11,9 +11,7 @@ use ra_syntax::{
}; };
use ra_text_edit::TextEdit; use ra_text_edit::TextEdit;
use crate::{SourceChange, SourceFileEdit}; pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> {
pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> {
let parse = db.parse(position.file_id); let parse = db.parse(position.file_id);
let file = parse.tree(); let file = parse.tree();
let comment = file let comment = file
@ -41,9 +39,7 @@ pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<Sour
let inserted = format!("\n{}{} $0", indent, prefix); let inserted = format!("\n{}{} $0", indent, prefix);
let edit = TextEdit::insert(position.offset, inserted); let edit = TextEdit::insert(position.offset, inserted);
let mut res = SourceChange::from(SourceFileEdit { edit, file_id: position.file_id }); Some(edit)
res.is_snippet = true;
Some(res)
} }
fn followed_by_comment(comment: &ast::Comment) -> bool { fn followed_by_comment(comment: &ast::Comment) -> bool {
@ -90,9 +86,8 @@ mod tests {
let (analysis, file_id) = single_file(&before); let (analysis, file_id) = single_file(&before);
let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?; let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?;
assert_eq!(result.source_file_edits.len(), 1);
let mut actual = before.to_string(); let mut actual = before.to_string();
result.source_file_edits[0].edit.apply(&mut actual); result.apply(&mut actual);
Some(actual) Some(actual)
} }

View file

@ -85,6 +85,7 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti
experimental: Some(json!({ experimental: Some(json!({
"joinLines": true, "joinLines": true,
"ssr": true, "ssr": true,
"onEnter": true,
})), })),
} }
} }

View file

@ -102,8 +102,8 @@ pub enum OnEnter {}
impl Request for OnEnter { impl Request for OnEnter {
type Params = lsp_types::TextDocumentPositionParams; type Params = lsp_types::TextDocumentPositionParams;
type Result = Option<SnippetWorkspaceEdit>; type Result = Option<Vec<SnippetTextEdit>>;
const METHOD: &'static str = "rust-analyzer/onEnter"; const METHOD: &'static str = "experimental/onEnter";
} }
pub enum Runnables {} pub enum Runnables {}

View file

@ -174,13 +174,17 @@ pub fn handle_join_lines(
pub fn handle_on_enter( pub fn handle_on_enter(
world: WorldSnapshot, world: WorldSnapshot,
params: lsp_types::TextDocumentPositionParams, params: lsp_types::TextDocumentPositionParams,
) -> Result<Option<lsp_ext::SnippetWorkspaceEdit>> { ) -> Result<Option<Vec<lsp_ext::SnippetTextEdit>>> {
let _p = profile("handle_on_enter"); let _p = profile("handle_on_enter");
let position = from_proto::file_position(&world, params)?; let position = from_proto::file_position(&world, params)?;
match world.analysis().on_enter(position)? { let edit = match world.analysis().on_enter(position)? {
None => Ok(None), None => return Ok(None),
Some(source_change) => to_proto::snippet_workspace_edit(&world, source_change).map(Some), 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`. // Don't forget to add new trigger characters to `ServerCapabilities` in `caps.rs`.

View file

@ -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() 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<lsp_ext::SnippetTextEdit> {
text_edit
.into_iter()
.map(|indel| self::snippet_text_edit(line_index, line_endings, is_snippet, indel))
.collect()
}
pub(crate) fn completion_item( pub(crate) fn completion_item(
line_index: &LineIndex, line_index: &LineIndex,
line_endings: LineEndings, line_endings: LineEndings,

View file

@ -473,23 +473,14 @@ fn main() {{}}
text_document: server.doc_id("src/m0.rs"), text_document: server.doc_id("src/m0.rs"),
position: Position { line: 0, character: 5 }, position: Position { line: 0, character: 5 },
}, },
json!({ json!([{
"documentChanges": [ "insertTextFormat": 2,
{ "newText": "\n/// $0",
"edits": [ "range": {
{ "end": { "character": 5, "line": 0 },
"insertTextFormat": 2, "start": { "character": 5, "line": 0 }
"newText": "\n/// $0",
"range": {
"end": { "character": 5, "line": 0 },
"start": { "character": 5, "line": 0 }
}
}
],
"textDocument": { "uri": "file:///[..]src/m0.rs", "version": null }
} }
] }]),
}),
); );
let elapsed = start.elapsed(); let elapsed = start.elapsed();
assert!(elapsed.as_millis() < 2000, "typing enter took {:?}", 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"), text_document: server.doc_id("src/main.rs"),
position: Position { line: 0, character: 8 }, position: Position { line: 0, character: 8 },
}, },
json!({ json!([{
"documentChanges": [ "insertTextFormat": 2,
{ "newText": "\r\n/// $0",
"edits": [ "range": {
{ "end": { "line": 0, "character": 8 },
"insertTextFormat": 2, "start": { "line": 0, "character": 8 }
"newText": "\r\n/// $0",
"range": {
"end": { "line": 0, "character": 8 },
"start": { "line": 0, "character": 8 }
}
}
],
"textDocument": { "uri": "file:///[..]src/main.rs", "version": null }
} }
] }]),
}),
); );
} }

View file

@ -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. 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. 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 <kbd>Enter</kbd> 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) ## Structural Search Replace (SSR)
**Server Capability:** `{ "ssr": boolean }` **Server Capability:** `{ "ssr": boolean }`

View file

@ -3,7 +3,7 @@ import * as lc from 'vscode-languageclient';
import * as ra from './rust-analyzer-api'; import * as ra from './rust-analyzer-api';
import { Ctx, Cmd } from './ctx'; import { Ctx, Cmd } from './ctx';
import { applySnippetWorkspaceEdit } from './snippets'; import { applySnippetWorkspaceEdit, applySnippetTextEdits } from './snippets';
import { spawnSync } from 'child_process'; import { spawnSync } from 'child_process';
import { RunnableQuickPick, selectRunnable, createTask } from './run'; import { RunnableQuickPick, selectRunnable, createTask } from './run';
import { AstInspector } from './ast_inspector'; import { AstInspector } from './ast_inspector';
@ -102,7 +102,7 @@ export function onEnter(ctx: Ctx): Cmd {
if (!editor || !client) return false; 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() }, textDocument: { uri: editor.document.uri.toString() },
position: client.code2ProtocolConverter.asPosition( position: client.code2ProtocolConverter.asPosition(
editor.selection.active, editor.selection.active,
@ -111,10 +111,10 @@ export function onEnter(ctx: Ctx): Cmd {
// client.logFailedRequest(OnEnterRequest.type, error); // client.logFailedRequest(OnEnterRequest.type, error);
return null; return null;
}); });
if (!change) return false; if (!lcEdits) return false;
const workspaceEdit = client.protocol2CodeConverter.asWorkspaceEdit(change); const edits = client.protocol2CodeConverter.asTextEdits(lcEdits);
await applySnippetWorkspaceEdit(workspaceEdit); await applySnippetTextEdits(editor, edits);
return true; return true;
} }

View file

@ -67,8 +67,7 @@ export interface JoinLinesParams {
} }
export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], unknown>('experimental/joinLines'); export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], unknown>('experimental/joinLines');
export const onEnter = new lc.RequestType<lc.TextDocumentPositionParams, lc.TextEdit[], unknown>('experimental/onEnter');
export const onEnter = request<lc.TextDocumentPositionParams, Option<lc.WorkspaceEdit>>("onEnter");
export interface RunnablesParams { export interface RunnablesParams {
textDocument: lc.TextDocumentIdentifier; textDocument: lc.TextDocumentIdentifier;

View file

@ -8,7 +8,10 @@ export async function applySnippetWorkspaceEdit(edit: vscode.WorkspaceEdit) {
const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString()); const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString());
if (!editor) return; if (!editor) return;
await applySnippetTextEdits(editor, edits);
}
export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vscode.TextEdit[]) {
let selection: vscode.Selection | undefined = undefined; let selection: vscode.Selection | undefined = undefined;
let lineDelta = 0; let lineDelta = 0;
await editor.edit((builder) => { await editor.edit((builder) => {