[3209] feat(lsp): range formatting

Add basic range formatting capabilities when multiple selection
are present.

Related: https://github.com/helix-editor/helix/issues/3209#issue-1318916766
pull/9156/head
Matouš Dzivjak 11 months ago
parent 22a051408a
commit 62f6bb3c3f
No known key found for this signature in database

@ -313,6 +313,7 @@ where
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum LanguageServerFeature { pub enum LanguageServerFeature {
Format, Format,
FormatSelection,
GotoDeclaration, GotoDeclaration,
GotoDefinition, GotoDefinition,
GotoTypeDefinition, GotoTypeDefinition,
@ -338,6 +339,7 @@ impl Display for LanguageServerFeature {
use LanguageServerFeature::*; use LanguageServerFeature::*;
let feature = match self { let feature = match self {
Format => "format", Format => "format",
FormatSelection => "format-selection",
GotoDeclaration => "goto-declaration", GotoDeclaration => "goto-declaration",
GotoDefinition => "goto-definition", GotoDefinition => "goto-definition",
GotoTypeDefinition => "goto-type-definition", GotoTypeDefinition => "goto-type-definition",

@ -287,6 +287,10 @@ impl Client {
capabilities.document_formatting_provider, capabilities.document_formatting_provider,
Some(OneOf::Left(true) | OneOf::Right(_)) Some(OneOf::Left(true) | OneOf::Right(_))
), ),
LanguageServerFeature::FormatSelection => matches!(
capabilities.document_range_formatting_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::GotoDeclaration => matches!( LanguageServerFeature::GotoDeclaration => matches!(
capabilities.declaration_provider, capabilities.declaration_provider,
Some( Some(
@ -662,6 +666,9 @@ impl Client {
dynamic_registration: Some(false), dynamic_registration: Some(false),
resolve_support: None, resolve_support: None,
}), }),
range_formatting: Some(lsp::DocumentFormattingClientCapabilities {
dynamic_registration: Some(false),
}),
..Default::default() ..Default::default()
}), }),
window: Some(lsp::WindowClientCapabilities { window: Some(lsp::WindowClientCapabilities {

@ -63,6 +63,7 @@ use crate::{
}; };
use crate::job::{self, Jobs}; use crate::job::{self, Jobs};
use futures_util::future::join_all;
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
@ -427,7 +428,7 @@ impl MappableCommand {
paste_primary_clipboard_before, "Paste primary clipboard before selections", paste_primary_clipboard_before, "Paste primary clipboard before selections",
indent, "Indent selection", indent, "Indent selection",
unindent, "Unindent selection", unindent, "Unindent selection",
format_selections, "Format selection", format_selections, "Format selections",
join_selections, "Join lines inside selection", join_selections, "Join lines inside selection",
join_selections_space, "Join lines inside selection and select spaces", join_selections_space, "Join lines inside selection and select spaces",
keep_selections, "Keep selections matching regex", keep_selections, "Keep selections matching regex",
@ -4469,25 +4470,11 @@ fn format_selections(cx: &mut Context) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let view_id = view.id; let view_id = view.id;
// via lsp if available // TODO: via lsp if available else via tree-sitter indentation calculations
// TODO: else via tree-sitter indentation calculations
if doc.selection(view_id).len() != 1 {
cx.editor
.set_error("format_selections only supports a single selection for now");
return;
}
// TODO extra LanguageServerFeature::FormatSelections?
// maybe such that LanguageServerFeature::Format contains it as well
let Some(language_server) = doc let Some(language_server) = doc
.language_servers_with_feature(LanguageServerFeature::Format) .language_servers_with_feature(LanguageServerFeature::FormatSelection)
.find(|ls| { .next()
matches!(
ls.capabilities().document_range_formatting_provider,
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
)
})
else { else {
cx.editor cx.editor
.set_error("No configured language server supports range formatting"); .set_error("No configured language server supports range formatting");
@ -4498,16 +4485,15 @@ fn format_selections(cx: &mut Context) {
let ranges: Vec<lsp::Range> = doc let ranges: Vec<lsp::Range> = doc
.selection(view_id) .selection(view_id)
.iter() .iter()
// request and process range formatting in reverse order from last selection
// to the first selection to reduce the chances of collisions (change in earlier
// sections could cause offsets in later sections, can't happen the other way around).
.rev()
.map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding)) .map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding))
.collect(); .collect();
// TODO: handle fails let futures = ranges.into_iter().filter_map(|range| {
// TODO: concurrent map over all ranges language_server.text_document_range_formatting(
let range = ranges[0];
let future = language_server
.text_document_range_formatting(
doc.identifier(), doc.identifier(),
range, range,
lsp::FormattingOptions { lsp::FormattingOptions {
@ -4517,12 +4503,25 @@ fn format_selections(cx: &mut Context) {
}, },
None, None,
) )
.unwrap(); });
let results = helix_lsp::block_on(join_all(futures));
let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default(); let all_edits = results
.into_iter()
.filter_map(|result| {
match result {
// TODO: handle colliding edits (edits outside the range) and edits that result into collision.
// See: https://github.com/helix-editor/helix/issues/3209#issuecomment-1197463913
Ok(edits) => Some(edits),
Err(_) => None,
}
})
.flatten()
.collect();
let transaction = let transaction =
helix_lsp::util::generate_transaction_from_edits(doc.text(), edits, offset_encoding); helix_lsp::util::generate_transaction_from_edits(doc.text(), all_edits, offset_encoding);
doc.apply(&transaction, view_id); doc.apply(&transaction, view_id);
} }

Loading…
Cancel
Save