mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-25 12:33:33 +00:00
Add on-enter handler
Now, typing doc comments is much more pleasant
This commit is contained in:
parent
82447ecace
commit
2b956fd3a8
12 changed files with 630 additions and 401 deletions
|
@ -184,6 +184,12 @@ impl Analysis {
|
|||
let file = self.imp.file_syntax(file_id);
|
||||
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> {
|
||||
let file = self.imp.file_syntax(file_id);
|
||||
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,
|
||||
introduce_variable,
|
||||
},
|
||||
typing::{join_lines, on_eq_typed},
|
||||
typing::{join_lines, on_eq_typed, on_enter},
|
||||
completion::{scope_completion, CompletionItem},
|
||||
folding_ranges::{Fold, FoldKind, folding_ranges}
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@ use ra_syntax::{
|
|||
TextUnit, TextRange, SyntaxNodeRef, File, AstNode, SyntaxKind,
|
||||
ast,
|
||||
algo::{
|
||||
find_covering_node,
|
||||
find_covering_node, find_leaf_at_offset, LeafAtOffset,
|
||||
},
|
||||
text_utils::{intersect, contains_offset_nonstrict},
|
||||
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> {
|
||||
let let_stmt: ast::LetStmt = find_node_at_offset(file.syntax(), offset)?;
|
||||
if let_stmt.has_semi() {
|
||||
|
@ -187,7 +239,7 @@ fn compute_ws(left: SyntaxNodeRef, right: SyntaxNodeRef) -> &'static str {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
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) {
|
||||
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,
|
||||
Some(pos) => {
|
||||
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 {
|
||||
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 {
|
||||
type Ctx = ServerWorld;
|
||||
type Output = TextDocumentEdit;
|
||||
|
|
|
@ -77,6 +77,20 @@ pub fn handle_join_lines(
|
|||
.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(
|
||||
world: ServerWorld,
|
||||
params: req::DocumentOnTypeFormattingParams,
|
||||
|
|
|
@ -244,6 +244,7 @@ fn on_request(
|
|||
.on::<req::ExtendSelection>(handlers::handle_extend_selection)?
|
||||
.on::<req::FindMatchingBrace>(handlers::handle_find_matching_brace)?
|
||||
.on::<req::JoinLines>(handlers::handle_join_lines)?
|
||||
.on::<req::OnEnter>(handlers::handle_on_enter)?
|
||||
.on::<req::OnTypeFormatting>(handlers::handle_on_type_formatting)?
|
||||
.on::<req::DocumentSymbolRequest>(handlers::handle_document_symbol)?
|
||||
.on::<req::WorkspaceSymbol>(handlers::handle_workspace_symbol)?
|
||||
|
|
|
@ -119,6 +119,14 @@ pub struct JoinLinesParams {
|
|||
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 {}
|
||||
|
||||
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
|
||||
and sometimes fall-backs to the horrible built-in highlighting)
|
||||
|
||||
|
||||
* **Go to symbol in workspace** (`ctrl+t`)
|
||||
- `#Foo` searches for `Foo` type 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
|
||||
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).
|
||||
|
||||
`<|>` signifies cursor position
|
||||
|
||||
|
||||
- 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 joinLines from './join_lines';
|
||||
import * as matchingBrace from './matching_brace';
|
||||
import * as on_enter from './on_enter';
|
||||
import * as parentModule from './parent_module';
|
||||
import * as runnables from './runnables';
|
||||
import * as syntaxTree from './syntaxTree';
|
||||
|
@ -13,5 +14,6 @@ export {
|
|||
matchingBrace,
|
||||
parentModule,
|
||||
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) {
|
||||
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
|
||||
registerCommand('ra-lsp.syntaxTree', commands.syntaxTree.handle);
|
||||
|
@ -27,11 +44,12 @@ export function activate(context: vscode.ExtensionContext) {
|
|||
'ra-lsp.applySourceChange',
|
||||
commands.applySourceChange.handle
|
||||
);
|
||||
overrideCommand('type', commands.on_enter.handle)
|
||||
|
||||
// Notifications are events triggered by the language server
|
||||
const allNotifications: Iterable<
|
||||
[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
|
||||
vscode.window.onDidChangeActiveTextEditor(
|
||||
|
|
Loading…
Reference in a new issue