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:
bors[bot] 2018-10-09 16:52:48 +00:00
commit 31c8ebb743
12 changed files with 630 additions and 401 deletions

View file

@ -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)?))

View file

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

View file

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

View file

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

View file

@ -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,

View file

@ -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)?

View file

@ -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 {

View file

@ -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 `,`

File diff suppressed because it is too large Load diff

View file

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

View 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
}

View file

@ -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,6 +44,7 @@ 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<