mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-25 12:33:33 +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,
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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, ¶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<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, ¶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,
|
||||
}]))
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in a new issue