mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-26 13:03:31 +00:00
Merge #4607
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:
commit
8686d0b0ac
11 changed files with 105 additions and 55 deletions
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 }
|
|
||||||
}
|
}
|
||||||
]
|
}]),
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 }`
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
Loading…
Reference in a new issue