Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Less rust-analyzer specific onEnter #4607

Merged
merged 1 commit into from
May 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/ra_ide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down
11 changes: 3 additions & 8 deletions crates/ra_ide/src/typing/on_enter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}

Expand Down
1 change: 1 addition & 0 deletions crates/rust-analyzer/src/caps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti
experimental: Some(json!({
"joinLines": true,
"ssr": true,
"onEnter": true,
})),
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/rust-analyzer/src/lsp_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
14 changes: 9 additions & 5 deletions crates/rust-analyzer/src/main_loop/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
12 changes: 12 additions & 0 deletions crates/rust-analyzer/src/to_proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
46 changes: 14 additions & 32 deletions crates/rust-analyzer/tests/heavy_tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 }
}
]
}),
}]),
);
}

Expand Down
53 changes: 53 additions & 0 deletions docs/dev/lsp-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }`
Expand Down
10 changes: 5 additions & 5 deletions editors/code/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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;
}

Expand Down
3 changes: 1 addition & 2 deletions editors/code/src/rust-analyzer-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions editors/code/src/snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down