Less rust-analyzer specific onEnter

This commit is contained in:
Aleksey Kladov 2020-05-25 14:12:53 +02:00
parent e4f91bfa57
commit 76e170c3d0
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
/// 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))
}

View file

@ -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<SourceChange> {
pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> {
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<Sour
let inserted = format!("\n{}{} $0", indent, prefix);
let edit = TextEdit::insert(position.offset, inserted);
let mut res = SourceChange::from(SourceFileEdit { edit, file_id: position.file_id });
res.is_snippet = true;
Some(res)
Some(edit)
}
fn followed_by_comment(comment: &ast::Comment) -> 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)
}

View file

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

View file

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

View file

@ -174,13 +174,17 @@ pub fn handle_join_lines(
pub fn handle_on_enter(
world: WorldSnapshot,
params: lsp_types::TextDocumentPositionParams,
) -> Result<Option<lsp_ext::SnippetWorkspaceEdit>> {
) -> Result<Option<Vec<lsp_ext::SnippetTextEdit>>> {
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`.

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()
}
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(
line_index: &LineIndex,
line_endings: LineEndings,

View file

@ -473,23 +473,14 @@ fn main() {{}}
text_document: server.doc_id("src/m0.rs"),
position: Position { line: 0, character: 5 },
},
json!({
"documentChanges": [
{
"edits": [
{
json!([{
"insertTextFormat": 2,
"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();
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": [
{
json!([{
"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 }
}
]
}),
}]),
);
}

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.
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)
**Server Capability:** `{ "ssr": boolean }`

View file

@ -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;
}

View file

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