Formalize JoinLines protocol extension

This commit is contained in:
Aleksey Kladov
2020-05-21 19:50:23 +02:00
parent ba6cf638fb
commit 5b5ebec440
9 changed files with 129 additions and 47 deletions
+3 -6
View File
@@ -89,6 +89,7 @@ macro_rules! eprintln {
symbol_index::Query,
RootDatabase,
};
pub use ra_text_edit::{Indel, TextEdit};
pub type Cancelable<T> = Result<T, Canceled>;
@@ -285,14 +286,10 @@ pub fn expand_macro(&self, position: FilePosition) -> Cancelable<Option<Expanded
/// Returns an edit to remove all newlines in the range, cleaning up minor
/// stuff like trailing commas.
pub fn join_lines(&self, frange: FileRange) -> Cancelable<SourceChange> {
pub fn join_lines(&self, frange: FileRange) -> Cancelable<TextEdit> {
self.with_db(|db| {
let parse = db.parse(frange.file_id);
let file_edit = SourceFileEdit {
file_id: frange.file_id,
edit: join_lines::join_lines(&parse.tree(), frange.range),
};
SourceChange::source_file_edit("Join lines", file_edit)
join_lines::join_lines(&parse.tree(), frange.range)
})
}
+23 -10
View File
@@ -17,7 +17,7 @@ pub struct Indel {
pub delete: TextRange,
}
#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct TextEdit {
indels: Vec<Indel>,
}
@@ -64,14 +64,6 @@ pub fn replace(range: TextRange, replace_with: String) -> TextEdit {
builder.finish()
}
pub(crate) fn from_indels(mut indels: Vec<Indel>) -> TextEdit {
indels.sort_by_key(|a| (a.delete.start(), a.delete.end()));
for (a1, a2) in indels.iter().zip(indels.iter().skip(1)) {
assert!(a1.delete.end() <= a2.delete.start())
}
TextEdit { indels }
}
pub fn len(&self) -> usize {
self.indels.len()
}
@@ -122,6 +114,17 @@ pub fn apply(&self, text: &mut String) {
*text = buf
}
pub fn union(&mut self, other: TextEdit) -> Result<(), TextEdit> {
// FIXME: can be done without allocating intermediate vector
let mut all = self.iter().chain(other.iter()).collect::<Vec<_>>();
if !check_disjoint(&mut all) {
return Err(other);
}
self.indels.extend(other.indels);
assert!(check_disjoint(&mut self.indels));
Ok(())
}
pub fn apply_to_offset(&self, offset: TextSize) -> Option<TextSize> {
let mut res = offset;
for indel in self.indels.iter() {
@@ -149,9 +152,19 @@ pub fn insert(&mut self, offset: TextSize, text: String) {
self.indels.push(Indel::insert(offset, text))
}
pub fn finish(self) -> TextEdit {
TextEdit::from_indels(self.indels)
let mut indels = self.indels;
assert!(check_disjoint(&mut indels));
TextEdit { indels }
}
pub fn invalidates_offset(&self, offset: TextSize) -> bool {
self.indels.iter().any(|indel| indel.delete.contains_inclusive(offset))
}
}
fn check_disjoint(indels: &mut [impl std::borrow::Borrow<Indel>]) -> bool {
indels.sort_by_key(|indel| (indel.borrow().delete.start(), indel.borrow().delete.end()));
indels
.iter()
.zip(indels.iter().skip(1))
.all(|(l, r)| l.borrow().delete.end() <= r.borrow().delete.start())
}
+6 -3
View File
@@ -1,8 +1,6 @@
//! Advertizes the capabilities of the LSP Server.
use std::env;
use crate::semantic_tokens;
use lsp_types::{
CallHierarchyServerCapability, CodeActionOptions, CodeActionProviderCapability,
CodeLensOptions, CompletionOptions, DocumentOnTypeFormattingOptions,
@@ -12,6 +10,9 @@
ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
TextDocumentSyncOptions, TypeDefinitionProviderCapability, WorkDoneProgressOptions,
};
use serde_json::json;
use crate::semantic_tokens;
pub fn server_capabilities() -> ServerCapabilities {
ServerCapabilities {
@@ -91,6 +92,8 @@ pub fn server_capabilities() -> ServerCapabilities {
}
.into(),
),
experimental: Default::default(),
experimental: Some(json!({
"joinLines": true,
})),
}
}
+3 -3
View File
@@ -87,15 +87,15 @@ pub enum JoinLines {}
impl Request for JoinLines {
type Params = JoinLinesParams;
type Result = SourceChange;
const METHOD: &'static str = "rust-analyzer/joinLines";
type Result = Vec<lsp_types::TextEdit>;
const METHOD: &'static str = "experimental/joinLines";
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct JoinLinesParams {
pub text_document: TextDocumentIdentifier,
pub range: Range,
pub ranges: Vec<Range>,
}
pub enum OnEnter {}
+22 -8
View File
@@ -15,10 +15,11 @@
DocumentSymbol, FoldingRange, FoldingRangeParams, Hover, HoverContents, Location,
MarkupContent, MarkupKind, Position, PrepareRenameResponse, Range, RenameParams,
SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult,
SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, TextEdit, Url, WorkspaceEdit,
SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, Url, WorkspaceEdit,
};
use ra_ide::{
Assist, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope,
TextEdit,
};
use ra_prof::profile;
use ra_project_model::TargetKind;
@@ -149,11 +150,24 @@ pub fn handle_find_matching_brace(
pub fn handle_join_lines(
world: WorldSnapshot,
params: lsp_ext::JoinLinesParams,
) -> Result<lsp_ext::SourceChange> {
) -> Result<Vec<lsp_types::TextEdit>> {
let _p = profile("handle_join_lines");
let frange = from_proto::file_range(&world, params.text_document, params.range)?;
let source_change = world.analysis().join_lines(frange)?;
to_proto::source_change(&world, source_change)
let file_id = from_proto::file_id(&world, &params.text_document.uri)?;
let line_index = world.analysis().file_line_index(file_id)?;
let line_endings = world.file_line_endings(file_id);
let mut res = TextEdit::default();
for range in params.ranges {
let range = from_proto::text_range(&line_index, range);
let edit = world.analysis().join_lines(FileRange { file_id, range })?;
match res.union(edit) {
Ok(()) => (),
Err(_edit) => {
// just ignore overlapping edits
}
}
}
let res = to_proto::text_edit_vec(&line_index, line_endings, res);
Ok(res)
}
pub fn handle_on_enter(
@@ -172,7 +186,7 @@ pub fn handle_on_enter(
pub fn handle_on_type_formatting(
world: WorldSnapshot,
params: lsp_types::DocumentOnTypeFormattingParams,
) -> Result<Option<Vec<TextEdit>>> {
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
let _p = profile("handle_on_type_formatting");
let mut position = from_proto::file_position(&world, params.text_document_position)?;
let line_index = world.analysis().file_line_index(position.file_id)?;
@@ -618,7 +632,7 @@ pub fn handle_references(
pub fn handle_formatting(
world: WorldSnapshot,
params: DocumentFormattingParams,
) -> Result<Option<Vec<TextEdit>>> {
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
let _p = profile("handle_formatting");
let file_id = from_proto::file_id(&world, &params.text_document.uri)?;
let file = world.analysis().file_text(file_id)?;
@@ -685,7 +699,7 @@ pub fn handle_formatting(
}
}
Ok(Some(vec![TextEdit {
Ok(Some(vec![lsp_types::TextEdit {
range: Range::new(Position::new(0, 0), end_position),
new_text: captured_stdout,
}]))
+3 -4
View File
@@ -2,12 +2,11 @@
use ra_db::{FileId, FileRange};
use ra_ide::{
Assist, CompletionItem, CompletionItemKind, Documentation, FileSystemEdit, Fold, FoldKind,
FunctionSignature, Highlight, HighlightModifier, HighlightTag, HighlightedRange, InlayHint,
InlayKind, InsertTextFormat, LineIndex, NavigationTarget, ReferenceAccess, Severity,
SourceChange, SourceFileEdit,
FunctionSignature, Highlight, HighlightModifier, HighlightTag, HighlightedRange, Indel,
InlayHint, InlayKind, InsertTextFormat, LineIndex, NavigationTarget, ReferenceAccess, Severity,
SourceChange, SourceFileEdit, TextEdit,
};
use ra_syntax::{SyntaxKind, TextRange, TextSize};
use ra_text_edit::{Indel, TextEdit};
use ra_vfs::LineEndings;
use crate::{lsp_ext, semantic_tokens, world::WorldSnapshot, Result};
+59 -7
View File
@@ -7,13 +7,7 @@ All capabilities are enabled via `experimental` field of `ClientCapabilities`.
## `SnippetTextEdit`
**Capability**
```typescript
{
"snippetTextEdit": boolean
}
```
**Client Capability:** `{ "snippetTextEdit": boolean }`
If this capability is set, `WorkspaceEdit`s returned from `codeAction` requests might contain `SnippetTextEdit`s instead of usual `TextEdit`s:
@@ -32,3 +26,61 @@ export interface TextDocumentEdit {
When applying such code action, the editor should insert snippet, with tab stops and placeholder.
At the moment, rust-analyzer guarantees that only a single edit will have `InsertTextFormat.Snippet`.
### Example
"Add `derive`" code action transforms `struct S;` into `#[derive($0)] struct S;`
### Unresolved Questions
* Where exactly are `SnippetTextEdit`s allowed (only in code actions at the moment)?
* Can snippets span multiple files (so far, no)?
## `joinLines`
**Server Capability:** `{ "joinLines": boolean }`
This request is send from client to server to handle "Join Lines" editor action.
**Method:** `experimental/JoinLines`
**Request:**
```typescript
interface JoinLinesParams {
textDocument: TextDocumentIdentifier,
/// Currently active selections/cursor offsets.
/// This is an array to support multiple cursors.
ranges: Range[],
}
```
**Response:**
```typescript
TextEdit[]
```
### Example
```rust
fn main() {
/*cursor here*/let x = {
92
};
}
```
`experimental/joinLines` yields (curly braces are automagiacally removed)
```rust
fn main() {
let x = 92;
}
```
### Unresolved Question
* What is the position of the cursor after `joinLines`?
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.
+8 -4
View File
@@ -1,7 +1,7 @@
import * as ra from '../rust-analyzer-api';
import * as lc from 'vscode-languageclient';
import { Ctx, Cmd } from '../ctx';
import { applySourceChange } from '../source_change';
export function joinLines(ctx: Ctx): Cmd {
return async () => {
@@ -9,10 +9,14 @@ export function joinLines(ctx: Ctx): Cmd {
const client = ctx.client;
if (!editor || !client) return;
const change = await client.sendRequest(ra.joinLines, {
range: client.code2ProtocolConverter.asRange(editor.selection),
const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
textDocument: { uri: editor.document.uri.toString() },
});
await applySourceChange(ctx, change);
editor.edit((builder) => {
client.protocol2CodeConverter.asTextEdits(items).forEach((edit) => {
builder.replace(edit.range, edit.newText);
});
});
};
}
+2 -2
View File
@@ -64,9 +64,9 @@ export const parentModule = request<lc.TextDocumentPositionParams, Vec<lc.Locati
export interface JoinLinesParams {
textDocument: lc.TextDocumentIdentifier;
range: lc.Range;
ranges: lc.Range[];
}
export const joinLines = request<JoinLinesParams, SourceChange>("joinLines");
export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], unknown>('experimental/joinLines');
export const onEnter = request<lc.TextDocumentPositionParams, Option<lc.WorkspaceEdit>>("onEnter");