mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-26 13:03:31 +00:00
Merge #106
106: Add on-enter handler r=matklad a=matklad Now, typing doc comments is much more pleasant Co-authored-by: Aleksey Kladov <aleksey.kladov@gmail.com>
This commit is contained in:
commit
31c8ebb743
12 changed files with 630 additions and 401 deletions
|
@ -184,6 +184,12 @@ impl Analysis {
|
||||||
let file = self.imp.file_syntax(file_id);
|
let file = self.imp.file_syntax(file_id);
|
||||||
SourceChange::from_local_edit(file_id, "join lines", ra_editor::join_lines(&file, range))
|
SourceChange::from_local_edit(file_id, "join lines", ra_editor::join_lines(&file, range))
|
||||||
}
|
}
|
||||||
|
pub fn on_enter(&self, file_id: FileId, offset: TextUnit) -> Option<SourceChange> {
|
||||||
|
let file = self.imp.file_syntax(file_id);
|
||||||
|
let edit = ra_editor::on_enter(&file, offset)?;
|
||||||
|
let res = SourceChange::from_local_edit(file_id, "on enter", edit);
|
||||||
|
Some(res)
|
||||||
|
}
|
||||||
pub fn on_eq_typed(&self, file_id: FileId, offset: TextUnit) -> Option<SourceChange> {
|
pub fn on_eq_typed(&self, file_id: FileId, offset: TextUnit) -> Option<SourceChange> {
|
||||||
let file = self.imp.file_syntax(file_id);
|
let file = self.imp.file_syntax(file_id);
|
||||||
Some(SourceChange::from_local_edit(file_id, "add semicolon", ra_editor::on_eq_typed(&file, offset)?))
|
Some(SourceChange::from_local_edit(file_id, "add semicolon", ra_editor::on_eq_typed(&file, offset)?))
|
||||||
|
|
|
@ -35,7 +35,7 @@ pub use self::{
|
||||||
flip_comma, add_derive, add_impl,
|
flip_comma, add_derive, add_impl,
|
||||||
introduce_variable,
|
introduce_variable,
|
||||||
},
|
},
|
||||||
typing::{join_lines, on_eq_typed},
|
typing::{join_lines, on_eq_typed, on_enter},
|
||||||
completion::{scope_completion, CompletionItem},
|
completion::{scope_completion, CompletionItem},
|
||||||
folding_ranges::{Fold, FoldKind, folding_ranges}
|
folding_ranges::{Fold, FoldKind, folding_ranges}
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@ use ra_syntax::{
|
||||||
TextUnit, TextRange, SyntaxNodeRef, File, AstNode, SyntaxKind,
|
TextUnit, TextRange, SyntaxNodeRef, File, AstNode, SyntaxKind,
|
||||||
ast,
|
ast,
|
||||||
algo::{
|
algo::{
|
||||||
find_covering_node,
|
find_covering_node, find_leaf_at_offset, LeafAtOffset,
|
||||||
},
|
},
|
||||||
text_utils::{intersect, contains_offset_nonstrict},
|
text_utils::{intersect, contains_offset_nonstrict},
|
||||||
SyntaxKind::*,
|
SyntaxKind::*,
|
||||||
|
@ -56,6 +56,58 @@ pub fn join_lines(file: &File, range: TextRange) -> LocalEdit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_enter(file: &File, offset: TextUnit) -> Option<LocalEdit> {
|
||||||
|
let comment = find_leaf_at_offset(file.syntax(), offset).left_biased().filter(|it| it.kind() == COMMENT)?;
|
||||||
|
let prefix = comment_preffix(comment)?;
|
||||||
|
if offset < comment.range().start() + TextUnit::of_str(prefix) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let indent = node_indent(file, comment)?;
|
||||||
|
let inserted = format!("\n{}{}", indent, prefix);
|
||||||
|
let cursor_position = offset + TextUnit::of_str(&inserted);
|
||||||
|
let mut edit = EditBuilder::new();
|
||||||
|
edit.insert(offset, inserted);
|
||||||
|
Some(LocalEdit {
|
||||||
|
edit: edit.finish(),
|
||||||
|
cursor_position: Some(cursor_position),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn comment_preffix(comment: SyntaxNodeRef) -> Option<&'static str> {
|
||||||
|
let text = comment.leaf_text().unwrap();
|
||||||
|
let res = if text.starts_with("///") {
|
||||||
|
"/// "
|
||||||
|
} else if text.starts_with("//!") {
|
||||||
|
"//! "
|
||||||
|
} else if text.starts_with("//") {
|
||||||
|
"// "
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_indent<'a>(file: &'a File, node: SyntaxNodeRef) -> Option<&'a str> {
|
||||||
|
let ws = match find_leaf_at_offset(file.syntax(), node.range().start()) {
|
||||||
|
LeafAtOffset::Between(l, r) => {
|
||||||
|
assert!(r == node);
|
||||||
|
l
|
||||||
|
}
|
||||||
|
LeafAtOffset::Single(n) => {
|
||||||
|
assert!(n == node);
|
||||||
|
return Some("")
|
||||||
|
}
|
||||||
|
LeafAtOffset::None => unreachable!(),
|
||||||
|
};
|
||||||
|
if ws.kind() != WHITESPACE {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let text = ws.leaf_text().unwrap();
|
||||||
|
let pos = text.as_str().rfind('\n').map(|it| it + 1).unwrap_or(0);
|
||||||
|
Some(&text[pos..])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_eq_typed(file: &File, offset: TextUnit) -> Option<LocalEdit> {
|
pub fn on_eq_typed(file: &File, offset: TextUnit) -> Option<LocalEdit> {
|
||||||
let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
|
let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
|
||||||
if let_stmt.has_semi() {
|
if let_stmt.has_semi() {
|
||||||
|
@ -187,7 +239,7 @@ fn compute_ws(left: SyntaxNodeRef, right: SyntaxNodeRef) -> &'static str {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use test_utils::{check_action, extract_range, extract_offset};
|
use test_utils::{check_action, extract_range, extract_offset, add_cursor};
|
||||||
|
|
||||||
fn check_join_lines(before: &str, after: &str) {
|
fn check_join_lines(before: &str, after: &str) {
|
||||||
check_action(before, after, |file, offset| {
|
check_action(before, after, |file, offset| {
|
||||||
|
@ -344,4 +396,49 @@ fn foo() {
|
||||||
// }
|
// }
|
||||||
// ");
|
// ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_on_enter() {
|
||||||
|
fn apply_on_enter(before: &str) -> Option<String> {
|
||||||
|
let (offset, before) = extract_offset(before);
|
||||||
|
let file = File::parse(&before);
|
||||||
|
let result = on_enter(&file, offset)?;
|
||||||
|
let actual = result.edit.apply(&before);
|
||||||
|
let actual = add_cursor(&actual, result.cursor_position.unwrap());
|
||||||
|
Some(actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_check(before: &str, after: &str) {
|
||||||
|
let actual = apply_on_enter(before).unwrap();
|
||||||
|
assert_eq_text!(after, &actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_check_noop(text: &str) {
|
||||||
|
assert!(apply_on_enter(text).is_none())
|
||||||
|
}
|
||||||
|
|
||||||
|
do_check(r"
|
||||||
|
/// Some docs<|>
|
||||||
|
fn foo() {
|
||||||
|
}
|
||||||
|
", r"
|
||||||
|
/// Some docs
|
||||||
|
/// <|>
|
||||||
|
fn foo() {
|
||||||
|
}
|
||||||
|
");
|
||||||
|
do_check(r"
|
||||||
|
impl S {
|
||||||
|
/// Some<|> docs.
|
||||||
|
fn foo() {}
|
||||||
|
}
|
||||||
|
", r"
|
||||||
|
impl S {
|
||||||
|
/// Some
|
||||||
|
/// <|> docs.
|
||||||
|
fn foo() {}
|
||||||
|
}
|
||||||
|
");
|
||||||
|
do_check_noop(r"<|>//! docz");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -190,9 +190,13 @@ impl TryConvWith for SourceChange {
|
||||||
None => None,
|
None => None,
|
||||||
Some(pos) => {
|
Some(pos) => {
|
||||||
let line_index = world.analysis().file_line_index(pos.file_id);
|
let line_index = world.analysis().file_line_index(pos.file_id);
|
||||||
|
let edits = self.source_file_edits.iter().find(|it| it.file_id == pos.file_id)
|
||||||
|
.map(|it| it.edits.as_slice()).unwrap_or(&[]);
|
||||||
|
let line_col = translate_offset_with_edit(&*line_index, pos.offset, edits);
|
||||||
|
let position = Position::new(line_col.line as u64, u32::from(line_col.col) as u64);
|
||||||
Some(TextDocumentPositionParams {
|
Some(TextDocumentPositionParams {
|
||||||
text_document: TextDocumentIdentifier::new(pos.file_id.try_conv_with(world)?),
|
text_document: TextDocumentIdentifier::new(pos.file_id.try_conv_with(world)?),
|
||||||
position: pos.offset.conv_with(&line_index),
|
position,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -207,6 +211,41 @@ impl TryConvWith for SourceChange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HACK: we should translate offset to line/column using linde_index *with edits applied*.
|
||||||
|
// A naive version of this function would be to apply `edits` to the original text,
|
||||||
|
// construct a new line index and use that, but it would be slow.
|
||||||
|
//
|
||||||
|
// Writing fast & correct version is issue #105, let's use a quick hack in the meantime
|
||||||
|
fn translate_offset_with_edit(
|
||||||
|
pre_edit_index: &LineIndex,
|
||||||
|
offset: TextUnit,
|
||||||
|
edits: &[AtomEdit],
|
||||||
|
) -> LineCol {
|
||||||
|
let fallback = pre_edit_index.line_col(offset);
|
||||||
|
let edit = match edits.first() {
|
||||||
|
None => return fallback,
|
||||||
|
Some(edit) => edit
|
||||||
|
};
|
||||||
|
let end_offset = edit.delete.start() + TextUnit::of_str(&edit.insert);
|
||||||
|
if !(edit.delete.start() <= offset && offset <= end_offset) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
let rel_offset = offset - edit.delete.start();
|
||||||
|
let in_edit_line_col = LineIndex::new(&edit.insert).line_col(rel_offset);
|
||||||
|
let edit_line_col = pre_edit_index.line_col(edit.delete.start());
|
||||||
|
if in_edit_line_col.line == 0 {
|
||||||
|
LineCol {
|
||||||
|
line: edit_line_col.line,
|
||||||
|
col: edit_line_col.col + in_edit_line_col.col,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LineCol {
|
||||||
|
line: edit_line_col.line + in_edit_line_col.line,
|
||||||
|
col: in_edit_line_col.col,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryConvWith for SourceFileEdit {
|
impl TryConvWith for SourceFileEdit {
|
||||||
type Ctx = ServerWorld;
|
type Ctx = ServerWorld;
|
||||||
type Output = TextDocumentEdit;
|
type Output = TextDocumentEdit;
|
||||||
|
|
|
@ -77,6 +77,20 @@ pub fn handle_join_lines(
|
||||||
.try_conv_with(&world)
|
.try_conv_with(&world)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle_on_enter(
|
||||||
|
world: ServerWorld,
|
||||||
|
params: req::TextDocumentPositionParams,
|
||||||
|
_token: JobToken,
|
||||||
|
) -> Result<Option<req::SourceChange>> {
|
||||||
|
let file_id = params.text_document.try_conv_with(&world)?;
|
||||||
|
let line_index = world.analysis().file_line_index(file_id);
|
||||||
|
let offset = params.position.conv_with(&line_index);
|
||||||
|
match world.analysis().on_enter(file_id, offset) {
|
||||||
|
None => Ok(None),
|
||||||
|
Some(edit) => Ok(Some(edit.try_conv_with(&world)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_on_type_formatting(
|
pub fn handle_on_type_formatting(
|
||||||
world: ServerWorld,
|
world: ServerWorld,
|
||||||
params: req::DocumentOnTypeFormattingParams,
|
params: req::DocumentOnTypeFormattingParams,
|
||||||
|
|
|
@ -244,6 +244,7 @@ fn on_request(
|
||||||
.on::<req::ExtendSelection>(handlers::handle_extend_selection)?
|
.on::<req::ExtendSelection>(handlers::handle_extend_selection)?
|
||||||
.on::<req::FindMatchingBrace>(handlers::handle_find_matching_brace)?
|
.on::<req::FindMatchingBrace>(handlers::handle_find_matching_brace)?
|
||||||
.on::<req::JoinLines>(handlers::handle_join_lines)?
|
.on::<req::JoinLines>(handlers::handle_join_lines)?
|
||||||
|
.on::<req::OnEnter>(handlers::handle_on_enter)?
|
||||||
.on::<req::OnTypeFormatting>(handlers::handle_on_type_formatting)?
|
.on::<req::OnTypeFormatting>(handlers::handle_on_type_formatting)?
|
||||||
.on::<req::DocumentSymbolRequest>(handlers::handle_document_symbol)?
|
.on::<req::DocumentSymbolRequest>(handlers::handle_document_symbol)?
|
||||||
.on::<req::WorkspaceSymbol>(handlers::handle_workspace_symbol)?
|
.on::<req::WorkspaceSymbol>(handlers::handle_workspace_symbol)?
|
||||||
|
|
|
@ -119,6 +119,14 @@ pub struct JoinLinesParams {
|
||||||
pub range: Range,
|
pub range: Range,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum OnEnter {}
|
||||||
|
|
||||||
|
impl Request for OnEnter {
|
||||||
|
type Params = TextDocumentPositionParams;
|
||||||
|
type Result = Option<SourceChange>;
|
||||||
|
const METHOD: &'static str = "m/onEnter";
|
||||||
|
}
|
||||||
|
|
||||||
pub enum Runnables {}
|
pub enum Runnables {}
|
||||||
|
|
||||||
impl Request for Runnables {
|
impl Request for Runnables {
|
||||||
|
|
|
@ -16,7 +16,7 @@ It's better to remove existing Rust plugins to avoid interference.
|
||||||
|
|
||||||
* syntax highlighting (LSP does not have API for it, so impl is hacky
|
* syntax highlighting (LSP does not have API for it, so impl is hacky
|
||||||
and sometimes fall-backs to the horrible built-in highlighting)
|
and sometimes fall-backs to the horrible built-in highlighting)
|
||||||
|
|
||||||
* **Go to symbol in workspace** (`ctrl+t`)
|
* **Go to symbol in workspace** (`ctrl+t`)
|
||||||
- `#Foo` searches for `Foo` type in the current workspace
|
- `#Foo` searches for `Foo` type in the current workspace
|
||||||
- `#foo#` searches for `foo` function in the current workspace
|
- `#foo#` searches for `foo` function in the current workspace
|
||||||
|
@ -44,9 +44,24 @@ It's better to remove existing Rust plugins to avoid interference.
|
||||||
outside of the test function, this re-runs the last test. Do bind
|
outside of the test function, this re-runs the last test. Do bind
|
||||||
this to a shortcut!
|
this to a shortcut!
|
||||||
|
|
||||||
|
* Typing assists
|
||||||
|
- typing `let =` tries to smartly add `;` if `=` is followed by an existing expression.
|
||||||
|
- Enter inside comments continues comment (`<|>` signifies cursor position):
|
||||||
|
|
||||||
|
```
|
||||||
|
/// Docs<|>
|
||||||
|
fn foo() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
/// Docs
|
||||||
|
/// <|>
|
||||||
|
fn foo() {}
|
||||||
|
```
|
||||||
|
|
||||||
* code actions (use `ctrl+.` to activate).
|
* code actions (use `ctrl+.` to activate).
|
||||||
|
|
||||||
`<|>` signifies cursor position
|
|
||||||
|
|
||||||
- Flip `,`
|
- Flip `,`
|
||||||
|
|
||||||
|
|
786
editors/code/package-lock.json
generated
786
editors/code/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -2,6 +2,7 @@ import * as applySourceChange from './apply_source_change';
|
||||||
import * as extendSelection from './extend_selection';
|
import * as extendSelection from './extend_selection';
|
||||||
import * as joinLines from './join_lines';
|
import * as joinLines from './join_lines';
|
||||||
import * as matchingBrace from './matching_brace';
|
import * as matchingBrace from './matching_brace';
|
||||||
|
import * as on_enter from './on_enter';
|
||||||
import * as parentModule from './parent_module';
|
import * as parentModule from './parent_module';
|
||||||
import * as runnables from './runnables';
|
import * as runnables from './runnables';
|
||||||
import * as syntaxTree from './syntaxTree';
|
import * as syntaxTree from './syntaxTree';
|
||||||
|
@ -13,5 +14,6 @@ export {
|
||||||
matchingBrace,
|
matchingBrace,
|
||||||
parentModule,
|
parentModule,
|
||||||
runnables,
|
runnables,
|
||||||
syntaxTree
|
syntaxTree,
|
||||||
|
on_enter,
|
||||||
};
|
};
|
||||||
|
|
29
editors/code/src/commands/on_enter.ts
Normal file
29
editors/code/src/commands/on_enter.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as lc from 'vscode-languageclient';
|
||||||
|
import { Server } from '../server';
|
||||||
|
import { handle as applySourceChange, SourceChange } from './apply_source_change';
|
||||||
|
|
||||||
|
interface OnEnterParams {
|
||||||
|
textDocument: lc.TextDocumentIdentifier;
|
||||||
|
position: lc.Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handle(event: { text: string }): Promise<boolean> {
|
||||||
|
const editor = vscode.window.activeTextEditor;
|
||||||
|
if (editor == null || editor.document.languageId !== 'rust' || event.text !== '\n') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const request: OnEnterParams = {
|
||||||
|
textDocument: { uri: editor.document.uri.toString() },
|
||||||
|
position: Server.client.code2ProtocolConverter.asPosition(editor.selection.active),
|
||||||
|
};
|
||||||
|
const change = await Server.client.sendRequest<undefined | SourceChange>(
|
||||||
|
'm/onEnter',
|
||||||
|
request
|
||||||
|
);
|
||||||
|
if (!change) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await applySourceChange(change);
|
||||||
|
return true
|
||||||
|
}
|
|
@ -15,6 +15,23 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
function registerCommand(name: string, f: any) {
|
function registerCommand(name: string, f: any) {
|
||||||
disposeOnDeactivation(vscode.commands.registerCommand(name, f));
|
disposeOnDeactivation(vscode.commands.registerCommand(name, f));
|
||||||
}
|
}
|
||||||
|
function overrideCommand(
|
||||||
|
|
||||||
|
name: string,
|
||||||
|
f: (...args: any[]) => Promise<boolean>,
|
||||||
|
) {
|
||||||
|
const defaultCmd = `default:${name}`;
|
||||||
|
const original = async (...args: any[]) => await vscode.commands.executeCommand(defaultCmd, ...args);
|
||||||
|
registerCommand(name, async (...args: any[]) => {
|
||||||
|
const editor = vscode.window.activeTextEditor;
|
||||||
|
if (!editor || !editor.document || editor.document.languageId !== 'rust') {
|
||||||
|
return await original(...args);
|
||||||
|
}
|
||||||
|
if (!await f(...args)) {
|
||||||
|
return await original(...args);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Commands are requests from vscode to the language server
|
// Commands are requests from vscode to the language server
|
||||||
registerCommand('ra-lsp.syntaxTree', commands.syntaxTree.handle);
|
registerCommand('ra-lsp.syntaxTree', commands.syntaxTree.handle);
|
||||||
|
@ -27,11 +44,12 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
'ra-lsp.applySourceChange',
|
'ra-lsp.applySourceChange',
|
||||||
commands.applySourceChange.handle
|
commands.applySourceChange.handle
|
||||||
);
|
);
|
||||||
|
overrideCommand('type', commands.on_enter.handle)
|
||||||
|
|
||||||
// Notifications are events triggered by the language server
|
// Notifications are events triggered by the language server
|
||||||
const allNotifications: Iterable<
|
const allNotifications: Iterable<
|
||||||
[string, lc.GenericNotificationHandler]
|
[string, lc.GenericNotificationHandler]
|
||||||
> = [['m/publishDecorations', notifications.publishDecorations.handle]];
|
> = [['m/publishDecorations', notifications.publishDecorations.handle]];
|
||||||
|
|
||||||
// The events below are plain old javascript events, triggered and handled by vscode
|
// The events below are plain old javascript events, triggered and handled by vscode
|
||||||
vscode.window.onDidChangeActiveTextEditor(
|
vscode.window.onDidChangeActiveTextEditor(
|
||||||
|
|
Loading…
Reference in a new issue