Skip to content

Commit 1ae3cb9

Browse files
committed
[3209] feat(lsp): range formatting
Add basic range formatting capabilities when multiple selection are present. Related: #3209 (comment)
1 parent dbac78b commit 1ae3cb9

File tree

3 files changed

+37
-28
lines changed

3 files changed

+37
-28
lines changed

helix-core/src/syntax.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ impl<'de> Deserialize<'de> for FileType {
235235
#[serde(rename_all = "kebab-case")]
236236
pub enum LanguageServerFeature {
237237
Format,
238+
FormatSelection,
238239
GotoDeclaration,
239240
GotoDefinition,
240241
GotoTypeDefinition,
@@ -260,6 +261,7 @@ impl Display for LanguageServerFeature {
260261
use LanguageServerFeature::*;
261262
let feature = match self {
262263
Format => "format",
264+
FormatSelection => "format-selection",
263265
GotoDeclaration => "goto-declaration",
264266
GotoDefinition => "goto-definition",
265267
GotoTypeDefinition => "goto-type-definition",

helix-lsp/src/client.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ impl Client {
297297
capabilities.document_formatting_provider,
298298
Some(OneOf::Left(true) | OneOf::Right(_))
299299
),
300+
LanguageServerFeature::FormatSelection => matches!(
301+
capabilities.document_range_formatting_provider,
302+
Some(OneOf::Left(true) | OneOf::Right(_))
303+
),
300304
LanguageServerFeature::GotoDeclaration => matches!(
301305
capabilities.declaration_provider,
302306
Some(
@@ -654,6 +658,12 @@ impl Client {
654658
dynamic_registration: Some(false),
655659
resolve_support: None,
656660
}),
661+
formatting: Some(lsp::DocumentFormattingClientCapabilities {
662+
dynamic_registration: Some(false),
663+
}),
664+
range_formatting: Some(lsp::DocumentFormattingClientCapabilities {
665+
dynamic_registration: Some(false),
666+
}),
657667
..Default::default()
658668
}),
659669
window: Some(lsp::WindowClientCapabilities {

helix-term/src/commands.rs

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ use crate::{
5555
};
5656

5757
use crate::job::{self, Jobs};
58+
use futures_util::future::join_all;
5859
use std::{
5960
collections::{HashMap, HashSet},
6061
fmt,
@@ -399,7 +400,7 @@ impl MappableCommand {
399400
paste_primary_clipboard_before, "Paste primary clipboard before selections",
400401
indent, "Indent selection",
401402
unindent, "Unindent selection",
402-
format_selections, "Format selection",
403+
format_selections, "Format selections",
403404
join_selections, "Join lines inside selection",
404405
join_selections_space, "Join lines inside selection and select spaces",
405406
keep_selections, "Keep selections matching regex",
@@ -4233,25 +4234,10 @@ fn format_selections(cx: &mut Context) {
42334234
let (view, doc) = current!(cx.editor);
42344235
let view_id = view.id;
42354236

4236-
// via lsp if available
4237-
// TODO: else via tree-sitter indentation calculations
4237+
// TODO: via lsp if available else via tree-sitter indentation calculations
42384238

4239-
if doc.selection(view_id).len() != 1 {
4240-
cx.editor
4241-
.set_error("format_selections only supports a single selection for now");
4242-
return;
4243-
}
4244-
4245-
// TODO extra LanguageServerFeature::FormatSelections?
4246-
// maybe such that LanguageServerFeature::Format contains it as well
42474239
let Some(language_server) = doc
4248-
.language_servers_with_feature(LanguageServerFeature::Format)
4249-
.find(|ls| {
4250-
matches!(
4251-
ls.capabilities().document_range_formatting_provider,
4252-
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
4253-
)
4254-
})
4240+
.language_servers_with_feature(LanguageServerFeature::FormatSelection).next()
42554241
else {
42564242
cx.editor
42574243
.set_error("No configured language server supports range formatting");
@@ -4262,27 +4248,38 @@ fn format_selections(cx: &mut Context) {
42624248
let ranges: Vec<lsp::Range> = doc
42634249
.selection(view_id)
42644250
.iter()
4251+
// request and process range formatting in reverse order from last selection
4252+
// to the first selection to reduce the chances of collisions.
4253+
.rev()
42654254
.map(|range| range_to_lsp_range(doc.text(), *range, offset_encoding))
42664255
.collect();
42674256

4268-
// TODO: handle fails
4269-
// TODO: concurrent map over all ranges
4270-
4271-
let range = ranges[0];
4272-
4273-
let future = language_server
4274-
.text_document_range_formatting(
4257+
let futures = ranges.into_iter().filter_map(|range| {
4258+
language_server.text_document_range_formatting(
42754259
doc.identifier(),
42764260
range,
42774261
lsp::FormattingOptions::default(),
42784262
None,
42794263
)
4280-
.unwrap();
4264+
});
42814265

4282-
let edits = tokio::task::block_in_place(|| helix_lsp::block_on(future)).unwrap_or_default();
4266+
let results = helix_lsp::block_on(join_all(futures));
4267+
4268+
let all_edits = results
4269+
.into_iter()
4270+
.filter_map(|result| {
4271+
match result {
4272+
// TODO: handle colliding edits (edits outside the range) and edits that result into collision.
4273+
// See: https://github.com/helix-editor/helix/issues/3209#issuecomment-1197463913
4274+
Ok(edits) => Some(edits),
4275+
Err(_) => None,
4276+
}
4277+
})
4278+
.flatten()
4279+
.collect();
42834280

42844281
let transaction =
4285-
helix_lsp::util::generate_transaction_from_edits(doc.text(), edits, offset_encoding);
4282+
helix_lsp::util::generate_transaction_from_edits(doc.text(), all_edits, offset_encoding);
42864283

42874284
doc.apply(&transaction, view_id);
42884285
}

0 commit comments

Comments
 (0)