mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-26 13:03:31 +00:00
Formalize JoinLines protocol extension
This commit is contained in:
parent
ba6cf638fb
commit
5b5ebec440
9 changed files with 129 additions and 47 deletions
|
@ -89,6 +89,7 @@ pub use ra_ide_db::{
|
||||||
symbol_index::Query,
|
symbol_index::Query,
|
||||||
RootDatabase,
|
RootDatabase,
|
||||||
};
|
};
|
||||||
|
pub use ra_text_edit::{Indel, TextEdit};
|
||||||
|
|
||||||
pub type Cancelable<T> = Result<T, Canceled>;
|
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
|
/// Returns an edit to remove all newlines in the range, cleaning up minor
|
||||||
/// stuff like trailing commas.
|
/// 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| {
|
self.with_db(|db| {
|
||||||
let parse = db.parse(frange.file_id);
|
let parse = db.parse(frange.file_id);
|
||||||
let file_edit = SourceFileEdit {
|
join_lines::join_lines(&parse.tree(), frange.range)
|
||||||
file_id: frange.file_id,
|
|
||||||
edit: join_lines::join_lines(&parse.tree(), frange.range),
|
|
||||||
};
|
|
||||||
SourceChange::source_file_edit("Join lines", file_edit)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ pub struct Indel {
|
||||||
pub delete: TextRange,
|
pub delete: TextRange,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct TextEdit {
|
pub struct TextEdit {
|
||||||
indels: Vec<Indel>,
|
indels: Vec<Indel>,
|
||||||
}
|
}
|
||||||
|
@ -64,14 +64,6 @@ impl TextEdit {
|
||||||
builder.finish()
|
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 {
|
pub fn len(&self) -> usize {
|
||||||
self.indels.len()
|
self.indels.len()
|
||||||
}
|
}
|
||||||
|
@ -122,6 +114,17 @@ impl TextEdit {
|
||||||
*text = buf
|
*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> {
|
pub fn apply_to_offset(&self, offset: TextSize) -> Option<TextSize> {
|
||||||
let mut res = offset;
|
let mut res = offset;
|
||||||
for indel in self.indels.iter() {
|
for indel in self.indels.iter() {
|
||||||
|
@ -149,9 +152,19 @@ impl TextEditBuilder {
|
||||||
self.indels.push(Indel::insert(offset, text))
|
self.indels.push(Indel::insert(offset, text))
|
||||||
}
|
}
|
||||||
pub fn finish(self) -> TextEdit {
|
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 {
|
pub fn invalidates_offset(&self, offset: TextSize) -> bool {
|
||||||
self.indels.iter().any(|indel| indel.delete.contains_inclusive(offset))
|
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())
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
//! Advertizes the capabilities of the LSP Server.
|
//! Advertizes the capabilities of the LSP Server.
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use crate::semantic_tokens;
|
|
||||||
|
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
CallHierarchyServerCapability, CodeActionOptions, CodeActionProviderCapability,
|
CallHierarchyServerCapability, CodeActionOptions, CodeActionProviderCapability,
|
||||||
CodeLensOptions, CompletionOptions, DocumentOnTypeFormattingOptions,
|
CodeLensOptions, CompletionOptions, DocumentOnTypeFormattingOptions,
|
||||||
|
@ -12,6 +10,9 @@ use lsp_types::{
|
||||||
ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
|
ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
|
||||||
TextDocumentSyncOptions, TypeDefinitionProviderCapability, WorkDoneProgressOptions,
|
TextDocumentSyncOptions, TypeDefinitionProviderCapability, WorkDoneProgressOptions,
|
||||||
};
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::semantic_tokens;
|
||||||
|
|
||||||
pub fn server_capabilities() -> ServerCapabilities {
|
pub fn server_capabilities() -> ServerCapabilities {
|
||||||
ServerCapabilities {
|
ServerCapabilities {
|
||||||
|
@ -91,6 +92,8 @@ pub fn server_capabilities() -> ServerCapabilities {
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
experimental: Default::default(),
|
experimental: Some(json!({
|
||||||
|
"joinLines": true,
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,15 +87,15 @@ pub enum JoinLines {}
|
||||||
|
|
||||||
impl Request for JoinLines {
|
impl Request for JoinLines {
|
||||||
type Params = JoinLinesParams;
|
type Params = JoinLinesParams;
|
||||||
type Result = SourceChange;
|
type Result = Vec<lsp_types::TextEdit>;
|
||||||
const METHOD: &'static str = "rust-analyzer/joinLines";
|
const METHOD: &'static str = "experimental/joinLines";
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct JoinLinesParams {
|
pub struct JoinLinesParams {
|
||||||
pub text_document: TextDocumentIdentifier,
|
pub text_document: TextDocumentIdentifier,
|
||||||
pub range: Range,
|
pub ranges: Vec<Range>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum OnEnter {}
|
pub enum OnEnter {}
|
||||||
|
|
|
@ -15,10 +15,11 @@ use lsp_types::{
|
||||||
DocumentSymbol, FoldingRange, FoldingRangeParams, Hover, HoverContents, Location,
|
DocumentSymbol, FoldingRange, FoldingRangeParams, Hover, HoverContents, Location,
|
||||||
MarkupContent, MarkupKind, Position, PrepareRenameResponse, Range, RenameParams,
|
MarkupContent, MarkupKind, Position, PrepareRenameResponse, Range, RenameParams,
|
||||||
SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult,
|
SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult,
|
||||||
SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, TextEdit, Url, WorkspaceEdit,
|
SemanticTokensResult, SymbolInformation, TextDocumentIdentifier, Url, WorkspaceEdit,
|
||||||
};
|
};
|
||||||
use ra_ide::{
|
use ra_ide::{
|
||||||
Assist, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope,
|
Assist, FileId, FilePosition, FileRange, Query, RangeInfo, Runnable, RunnableKind, SearchScope,
|
||||||
|
TextEdit,
|
||||||
};
|
};
|
||||||
use ra_prof::profile;
|
use ra_prof::profile;
|
||||||
use ra_project_model::TargetKind;
|
use ra_project_model::TargetKind;
|
||||||
|
@ -149,11 +150,24 @@ pub fn handle_find_matching_brace(
|
||||||
pub fn handle_join_lines(
|
pub fn handle_join_lines(
|
||||||
world: WorldSnapshot,
|
world: WorldSnapshot,
|
||||||
params: lsp_ext::JoinLinesParams,
|
params: lsp_ext::JoinLinesParams,
|
||||||
) -> Result<lsp_ext::SourceChange> {
|
) -> Result<Vec<lsp_types::TextEdit>> {
|
||||||
let _p = profile("handle_join_lines");
|
let _p = profile("handle_join_lines");
|
||||||
let frange = from_proto::file_range(&world, params.text_document, params.range)?;
|
let file_id = from_proto::file_id(&world, ¶ms.text_document.uri)?;
|
||||||
let source_change = world.analysis().join_lines(frange)?;
|
let line_index = world.analysis().file_line_index(file_id)?;
|
||||||
to_proto::source_change(&world, source_change)
|
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(
|
pub fn handle_on_enter(
|
||||||
|
@ -172,7 +186,7 @@ pub fn handle_on_enter(
|
||||||
pub fn handle_on_type_formatting(
|
pub fn handle_on_type_formatting(
|
||||||
world: WorldSnapshot,
|
world: WorldSnapshot,
|
||||||
params: lsp_types::DocumentOnTypeFormattingParams,
|
params: lsp_types::DocumentOnTypeFormattingParams,
|
||||||
) -> Result<Option<Vec<TextEdit>>> {
|
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
|
||||||
let _p = profile("handle_on_type_formatting");
|
let _p = profile("handle_on_type_formatting");
|
||||||
let mut position = from_proto::file_position(&world, params.text_document_position)?;
|
let mut position = from_proto::file_position(&world, params.text_document_position)?;
|
||||||
let line_index = world.analysis().file_line_index(position.file_id)?;
|
let line_index = world.analysis().file_line_index(position.file_id)?;
|
||||||
|
@ -618,7 +632,7 @@ pub fn handle_references(
|
||||||
pub fn handle_formatting(
|
pub fn handle_formatting(
|
||||||
world: WorldSnapshot,
|
world: WorldSnapshot,
|
||||||
params: DocumentFormattingParams,
|
params: DocumentFormattingParams,
|
||||||
) -> Result<Option<Vec<TextEdit>>> {
|
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
|
||||||
let _p = profile("handle_formatting");
|
let _p = profile("handle_formatting");
|
||||||
let file_id = from_proto::file_id(&world, ¶ms.text_document.uri)?;
|
let file_id = from_proto::file_id(&world, ¶ms.text_document.uri)?;
|
||||||
let file = world.analysis().file_text(file_id)?;
|
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),
|
range: Range::new(Position::new(0, 0), end_position),
|
||||||
new_text: captured_stdout,
|
new_text: captured_stdout,
|
||||||
}]))
|
}]))
|
||||||
|
|
|
@ -2,12 +2,11 @@
|
||||||
use ra_db::{FileId, FileRange};
|
use ra_db::{FileId, FileRange};
|
||||||
use ra_ide::{
|
use ra_ide::{
|
||||||
Assist, CompletionItem, CompletionItemKind, Documentation, FileSystemEdit, Fold, FoldKind,
|
Assist, CompletionItem, CompletionItemKind, Documentation, FileSystemEdit, Fold, FoldKind,
|
||||||
FunctionSignature, Highlight, HighlightModifier, HighlightTag, HighlightedRange, InlayHint,
|
FunctionSignature, Highlight, HighlightModifier, HighlightTag, HighlightedRange, Indel,
|
||||||
InlayKind, InsertTextFormat, LineIndex, NavigationTarget, ReferenceAccess, Severity,
|
InlayHint, InlayKind, InsertTextFormat, LineIndex, NavigationTarget, ReferenceAccess, Severity,
|
||||||
SourceChange, SourceFileEdit,
|
SourceChange, SourceFileEdit, TextEdit,
|
||||||
};
|
};
|
||||||
use ra_syntax::{SyntaxKind, TextRange, TextSize};
|
use ra_syntax::{SyntaxKind, TextRange, TextSize};
|
||||||
use ra_text_edit::{Indel, TextEdit};
|
|
||||||
use ra_vfs::LineEndings;
|
use ra_vfs::LineEndings;
|
||||||
|
|
||||||
use crate::{lsp_ext, semantic_tokens, world::WorldSnapshot, Result};
|
use crate::{lsp_ext, semantic_tokens, world::WorldSnapshot, Result};
|
||||||
|
|
|
@ -7,13 +7,7 @@ All capabilities are enabled via `experimental` field of `ClientCapabilities`.
|
||||||
|
|
||||||
## `SnippetTextEdit`
|
## `SnippetTextEdit`
|
||||||
|
|
||||||
**Capability**
|
**Client Capability:** `{ "snippetTextEdit": boolean }`
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
"snippetTextEdit": boolean
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If this capability is set, `WorkspaceEdit`s returned from `codeAction` requests might contain `SnippetTextEdit`s instead of usual `TextEdit`s:
|
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.
|
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`.
|
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.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as ra from '../rust-analyzer-api';
|
import * as ra from '../rust-analyzer-api';
|
||||||
|
import * as lc from 'vscode-languageclient';
|
||||||
|
|
||||||
import { Ctx, Cmd } from '../ctx';
|
import { Ctx, Cmd } from '../ctx';
|
||||||
import { applySourceChange } from '../source_change';
|
|
||||||
|
|
||||||
export function joinLines(ctx: Ctx): Cmd {
|
export function joinLines(ctx: Ctx): Cmd {
|
||||||
return async () => {
|
return async () => {
|
||||||
|
@ -9,10 +9,14 @@ export function joinLines(ctx: Ctx): Cmd {
|
||||||
const client = ctx.client;
|
const client = ctx.client;
|
||||||
if (!editor || !client) return;
|
if (!editor || !client) return;
|
||||||
|
|
||||||
const change = await client.sendRequest(ra.joinLines, {
|
const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
|
||||||
range: client.code2ProtocolConverter.asRange(editor.selection),
|
ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
|
||||||
textDocument: { uri: editor.document.uri.toString() },
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,9 +64,9 @@ export const parentModule = request<lc.TextDocumentPositionParams, Vec<lc.Locati
|
||||||
|
|
||||||
export interface JoinLinesParams {
|
export interface JoinLinesParams {
|
||||||
textDocument: lc.TextDocumentIdentifier;
|
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");
|
export const onEnter = request<lc.TextDocumentPositionParams, Option<lc.WorkspaceEdit>>("onEnter");
|
||||||
|
|
Loading…
Reference in a new issue