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

View file

@ -89,6 +89,7 @@ pub use ra_ide_db::{
symbol_index::Query,
RootDatabase,
};
pub use ra_text_edit::{Indel, TextEdit};
pub type Cancelable<T> = Result<T, Canceled>;
@ -285,14 +286,10 @@ impl Analysis {
/// 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)
})
}

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 @@ impl 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 @@ impl TextEdit {
*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 @@ impl TextEditBuilder {
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())
}

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 @@ use lsp_types::{
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,
})),
}
}

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

View file

@ -15,10 +15,11 @@ use lsp_types::{
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,
}]))

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

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.

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

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