Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Formalize JoinLines protocol extension #4557

Merged
merged 1 commit into from
May 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions crates/ra_ide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>;

Expand Down Expand Up @@ -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)
})
}

Expand Down
33 changes: 23 additions & 10 deletions crates/ra_text_edit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub struct Indel {
pub delete: TextRange,
}

#[derive(Debug, Clone)]
#[derive(Default, Debug, Clone)]
pub struct TextEdit {
indels: Vec<Indel>,
}
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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())
}
9 changes: 6 additions & 3 deletions crates/rust-analyzer/src/caps.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -91,6 +92,8 @@ pub fn server_capabilities() -> ServerCapabilities {
}
.into(),
),
experimental: Default::default(),
experimental: Some(json!({
"joinLines": true,
})),
}
}
6 changes: 3 additions & 3 deletions crates/rust-analyzer/src/lsp_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down
30 changes: 22 additions & 8 deletions crates/rust-analyzer/src/main_loop/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -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)?;
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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,
}]))
Expand Down
7 changes: 3 additions & 4 deletions crates/rust-analyzer/src/to_proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
66 changes: 59 additions & 7 deletions docs/dev/lsp-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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.
12 changes: 8 additions & 4 deletions editors/code/src/commands/join_lines.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
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 () => {
const editor = ctx.activeRustEditor;
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);
});
});
};
}
4 changes: 2 additions & 2 deletions editors/code/src/rust-analyzer-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down