diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs index d0aeb3ba72..97ff67ee89 100644 --- a/crates/ra_ide/src/lib.rs +++ b/crates/ra_ide/src/lib.rs @@ -89,6 +89,7 @@ pub use ra_ide_db::{ symbol_index::Query, RootDatabase, }; +pub use ra_text_edit::{Indel, TextEdit}; pub type Cancelable = Result; @@ -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 { + pub fn join_lines(&self, frange: FileRange) -> Cancelable { 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) }) } diff --git a/crates/ra_text_edit/src/lib.rs b/crates/ra_text_edit/src/lib.rs index 199fd10968..25554f583e 100644 --- a/crates/ra_text_edit/src/lib.rs +++ b/crates/ra_text_edit/src/lib.rs @@ -17,7 +17,7 @@ pub struct Indel { pub delete: TextRange, } -#[derive(Debug, Clone)] +#[derive(Default, Debug, Clone)] pub struct TextEdit { indels: Vec, } @@ -64,14 +64,6 @@ impl TextEdit { builder.finish() } - pub(crate) fn from_indels(mut indels: Vec) -> 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::>(); + 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 { 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]) -> 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()) +} diff --git a/crates/rust-analyzer/src/caps.rs b/crates/rust-analyzer/src/caps.rs index 110c9a4429..4c417c2704 100644 --- a/crates/rust-analyzer/src/caps.rs +++ b/crates/rust-analyzer/src/caps.rs @@ -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, + })), } } diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs index 3c7bd609d2..1bb1b02ab4 100644 --- a/crates/rust-analyzer/src/lsp_ext.rs +++ b/crates/rust-analyzer/src/lsp_ext.rs @@ -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; + 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, } pub enum OnEnter {} diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs index fcf08cd798..1219647188 100644 --- a/crates/rust-analyzer/src/main_loop/handlers.rs +++ b/crates/rust-analyzer/src/main_loop/handlers.rs @@ -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 { +) -> Result> { 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, ¶ms.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>> { +) -> Result>> { 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>> { +) -> Result>> { let _p = profile("handle_formatting"); let file_id = from_proto::file_id(&world, ¶ms.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, }])) diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs index 6171979634..f6f4bb1340 100644 --- a/crates/rust-analyzer/src/to_proto.rs +++ b/crates/rust-analyzer/src/to_proto.rs @@ -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}; diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index d2ec6c0215..0e3a0af1cb 100644 --- a/docs/dev/lsp-extensions.md +++ b/docs/dev/lsp-extensions.md @@ -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. diff --git a/editors/code/src/commands/join_lines.ts b/editors/code/src/commands/join_lines.ts index de0614653d..0bf1ee6e67 100644 --- a/editors/code/src/commands/join_lines.ts +++ b/editors/code/src/commands/join_lines.ts @@ -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); + }); + }); }; } diff --git a/editors/code/src/rust-analyzer-api.ts b/editors/code/src/rust-analyzer-api.ts index 3b83b10e38..8ed56c173e 100644 --- a/editors/code/src/rust-analyzer-api.ts +++ b/editors/code/src/rust-analyzer-api.ts @@ -64,9 +64,9 @@ export const parentModule = request("joinLines"); +export const joinLines = new lc.RequestType('experimental/joinLines'); export const onEnter = request>("onEnter");