From fd441ece2ba5bdadc5a0058c96e124beff3f94a8 Mon Sep 17 00:00:00 2001 From: Jonas Zeltner Date: Sun, 14 Sep 2025 23:49:30 +0200 Subject: [PATCH 1/8] Reply with correct server information --- src/cli/lsp.rs | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/cli/lsp.rs b/src/cli/lsp.rs index 4014d7fb..2c5445a6 100644 --- a/src/cli/lsp.rs +++ b/src/cli/lsp.rs @@ -4,8 +4,9 @@ use lsp_server::{Connection, ErrorCode, Message, Response}; use lsp_textdocument::{FullTextDocument, TextDocuments}; use lsp_types::{ request::{Formatting, RangeFormatting, Request}, - DocumentFormattingParams, DocumentRangeFormattingParams, OneOf, Position, Range, - ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Uri, + DocumentFormattingParams, DocumentRangeFormattingParams, InitializeResult, OneOf, Position, + Range, ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, + TextEdit, Uri, }; use stylua_lib::{format_code, OutputVerification}; @@ -125,14 +126,22 @@ fn handle_request( } fn main_loop(connection: Connection, config_resolver: &mut ConfigResolver) -> anyhow::Result<()> { - let capabilities = ServerCapabilities { - document_formatting_provider: Some(OneOf::Left(true)), - text_document_sync: Some(TextDocumentSyncCapability::Kind( - TextDocumentSyncKind::INCREMENTAL, - )), - ..Default::default() + let initialize_result = InitializeResult { + capabilities: ServerCapabilities { + document_formatting_provider: Some(OneOf::Left(true)), + text_document_sync: Some(TextDocumentSyncCapability::Kind( + TextDocumentSyncKind::INCREMENTAL, + )), + ..Default::default() + }, + server_info: Some(ServerInfo { + name: "stylua".to_string(), + version: Some(env!("CARGO_PKG_VERSION").to_string()), + }), }; - connection.initialize(serde_json::to_value(capabilities)?)?; + + let (id, _) = connection.initialize_start()?; + connection.initialize_finish(id, serde_json::to_value(initialize_result)?)?; let mut documents = TextDocuments::new(); for msg in &connection.receiver { @@ -184,7 +193,9 @@ mod tests { FormattingOptions, InitializeParams, Position, Range, TextDocumentIdentifier, TextDocumentItem, TextEdit, Uri, WorkDoneProgressParams, }; - use lsp_types::{OneOf, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind}; + use lsp_types::{ + OneOf, ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, + }; use serde::de::DeserializeOwned; use serde_json::to_value; @@ -235,7 +246,12 @@ mod tests { TextDocumentSyncKind::INCREMENTAL, )), ..Default::default() - }}) => {} + }, + "serverInfo": Some(ServerInfo { + name: "stylua".to_string(), + version: Some(env!("CARGO_PKG_VERSION").to_string()), + }), + }) => {} _ => panic!("assertion failed"), } } From 87e21dacb1ff288158e15f3a651b9daf70c2d368 Mon Sep 17 00:00:00 2001 From: Jonas Zeltner Date: Mon, 15 Sep 2025 00:55:18 +0200 Subject: [PATCH 2/8] Compute more granular textedits for formatting requests --- src/cli/lsp.rs | 99 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/src/cli/lsp.rs b/src/cli/lsp.rs index 2c5445a6..fdf06668 100644 --- a/src/cli/lsp.rs +++ b/src/cli/lsp.rs @@ -8,10 +8,87 @@ use lsp_types::{ Range, ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Uri, }; +use similar::{DiffOp, TextDiff}; use stylua_lib::{format_code, OutputVerification}; use crate::{config::ConfigResolver, opt}; +fn diffop_to_textedit(op: DiffOp, formatted_contents: &str) -> Option { + match op { + DiffOp::Equal { + old_index: _, + new_index: _, + len: _, + } => None, + DiffOp::Delete { + old_index, + old_len, + new_index: _, + } => Some(TextEdit { + range: Range { + start: Position { + line: old_index.try_into().expect("usize fits into u32"), + character: 0, + }, + end: Position { + line: (old_index + old_len) + .try_into() + .expect("usize fits into u32"), + character: 0, + }, + }, + new_text: String::new(), + }), + DiffOp::Insert { + old_index, + new_index, + new_len, + } => { + let insert_position = Position { + line: old_index.try_into().expect("usize fits into u32"), + character: 0, + }; + Some(TextEdit { + range: Range { + start: insert_position, + end: insert_position, + }, + new_text: formatted_contents + .lines() + .skip(new_index) + .take(new_len) + .collect::>() + .join("\n"), + }) + } + DiffOp::Replace { + old_index, + old_len, + new_index, + new_len, + } => Some(TextEdit { + range: Range { + start: Position { + line: old_index.try_into().expect("usize fits into u32"), + character: 0, + }, + end: Position { + line: (old_index + old_len) + .try_into() + .expect("usize fits into u32"), + character: 0, + }, + }, + new_text: formatted_contents + .lines() + .skip(new_index) + .take(new_len) + .collect::>() + .join("\n"), + }), + } +} + fn handle_formatting( uri: &Uri, document: &FullTextDocument, @@ -30,18 +107,16 @@ fn handle_formatting( let formatted_contents = format_code(contents, config, range, OutputVerification::None).ok()?; - let last_line_idx = document.line_count().saturating_sub(1); - let last_line_offset = document.offset_at(Position::new(last_line_idx, 0)); - let last_col = document.content_len() - last_line_offset; - - // TODO: We can be smarter about this in the future, and update only the parts that changed (using output_diff) - Some(vec![TextEdit { - range: Range { - start: Position::new(0, 0), - end: Position::new(last_line_idx, last_col), - }, - new_text: formatted_contents, - }]) + let operations = TextDiff::from_lines(contents, &formatted_contents).grouped_ops(0); + let edits = operations + .into_iter() + .flat_map(|operations| { + operations + .into_iter() + .filter_map(|op| diffop_to_textedit(op, &formatted_contents)) + }) + .collect(); + Some(edits) } fn handle_request( From 907fea11d1b5c9744d9353df10f9c6f3fe6c4de4 Mon Sep 17 00:00:00 2001 From: Jonas Zeltner Date: Mon, 15 Sep 2025 02:33:33 +0200 Subject: [PATCH 3/8] Use byte diff instead of line diff --- Cargo.lock | 14 ++++- Cargo.toml | 2 +- src/cli/lsp.rs | 136 ++++++++++++++++++++++--------------------------- 3 files changed, 73 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 32db88b3..51d04110 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,7 +36,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00ad3f3a942eee60335ab4342358c161ee296829e0d16ff42fc1d6cb07815467" dependencies = [ "anstyle", - "bstr", + "bstr 1.9.0", "doc-comment", "predicates", "predicates-core", @@ -97,6 +97,15 @@ dependencies = [ "cfg_aliases", ] +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + [[package]] name = "bstr" version = "1.9.0" @@ -418,7 +427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" dependencies = [ "aho-corasick", - "bstr", + "bstr 1.9.0", "log", "regex-automata", "regex-syntax", @@ -914,6 +923,7 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" dependencies = [ + "bstr 0.2.17", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 22273cee..5217dac9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ num_cpus = "1.16.0" regex = "1.10.2" serde = "1.0.188" serde_json = "1.0.108" -similar = { version = "2.3.0", features = ["text", "inline", "serde"] } +similar = { version = "2.3.0", features = ["text", "inline", "serde", "bytes"] } strum = { version = "0.25.0", features = ["derive"], optional = true } thiserror = "1.0.49" threadpool = "1.8.1" diff --git a/src/cli/lsp.rs b/src/cli/lsp.rs index fdf06668..aab104d5 100644 --- a/src/cli/lsp.rs +++ b/src/cli/lsp.rs @@ -4,16 +4,27 @@ use lsp_server::{Connection, ErrorCode, Message, Response}; use lsp_textdocument::{FullTextDocument, TextDocuments}; use lsp_types::{ request::{Formatting, RangeFormatting, Request}, - DocumentFormattingParams, DocumentRangeFormattingParams, InitializeResult, OneOf, Position, - Range, ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, - TextEdit, Uri, + DocumentFormattingParams, DocumentRangeFormattingParams, InitializeResult, OneOf, Range, + ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, + Uri, }; use similar::{DiffOp, TextDiff}; use stylua_lib::{format_code, OutputVerification}; use crate::{config::ConfigResolver, opt}; -fn diffop_to_textedit(op: DiffOp, formatted_contents: &str) -> Option { +fn diffop_to_textedit( + op: DiffOp, + document: &FullTextDocument, + formatted_contents: &str, +) -> Option { + let range = |start: usize, len: usize| Range { + start: document.position_at(start.try_into().expect("usize fits into u32")), + end: document.position_at((start + len).try_into().expect("usize fits into u32")), + }; + + let lookup = |start: usize, len: usize| formatted_contents[start..start + len].to_string(); + match op { DiffOp::Equal { old_index: _, @@ -25,66 +36,25 @@ fn diffop_to_textedit(op: DiffOp, formatted_contents: &str) -> Option old_len, new_index: _, } => Some(TextEdit { - range: Range { - start: Position { - line: old_index.try_into().expect("usize fits into u32"), - character: 0, - }, - end: Position { - line: (old_index + old_len) - .try_into() - .expect("usize fits into u32"), - character: 0, - }, - }, + range: range(old_index, old_len), new_text: String::new(), }), DiffOp::Insert { old_index, new_index, new_len, - } => { - let insert_position = Position { - line: old_index.try_into().expect("usize fits into u32"), - character: 0, - }; - Some(TextEdit { - range: Range { - start: insert_position, - end: insert_position, - }, - new_text: formatted_contents - .lines() - .skip(new_index) - .take(new_len) - .collect::>() - .join("\n"), - }) - } + } => Some(TextEdit { + range: range(old_index, 0), + new_text: lookup(new_index, new_len), + }), DiffOp::Replace { old_index, old_len, new_index, new_len, } => Some(TextEdit { - range: Range { - start: Position { - line: old_index.try_into().expect("usize fits into u32"), - character: 0, - }, - end: Position { - line: (old_index + old_len) - .try_into() - .expect("usize fits into u32"), - character: 0, - }, - }, - new_text: formatted_contents - .lines() - .skip(new_index) - .take(new_len) - .collect::>() - .join("\n"), + range: range(old_index, old_len), + new_text: lookup(new_index, new_len), }), } } @@ -107,13 +77,14 @@ fn handle_formatting( let formatted_contents = format_code(contents, config, range, OutputVerification::None).ok()?; - let operations = TextDiff::from_lines(contents, &formatted_contents).grouped_ops(0); + let operations = + TextDiff::from_chars(contents.as_bytes(), formatted_contents.as_bytes()).grouped_ops(0); let edits = operations .into_iter() .flat_map(|operations| { operations .into_iter() - .filter_map(|op| diffop_to_textedit(op, &formatted_contents)) + .filter_map(|op| diffop_to_textedit(op, document, &formatted_contents)) }) .collect(); Some(edits) @@ -254,6 +225,8 @@ pub fn run(opt: opt::Opt) -> anyhow::Result<()> { #[cfg(test)] mod tests { + use std::cmp::Ordering; + use std::convert::TryInto; use std::str::FromStr; use clap::Parser; @@ -373,6 +346,35 @@ mod tests { assert!(client.receiver.is_empty()); } + fn with_edits(text: &str, mut edits: Vec) -> String { + edits.sort_by(|a, b| match a.range.start.line.cmp(&b.range.start.line) { + Ordering::Equal => a + .range + .start + .character + .cmp(&b.range.start.character) + .reverse(), + order => order.reverse(), + }); + let mut text = text.to_string(); + for edit in edits { + let start = text + .lines() + .take(edit.range.start.line.try_into().unwrap()) + .map(|line| line.len() + '\n'.len_utf8()) + .sum::() + + >::try_into(edit.range.start.character).unwrap(); + let end = text + .lines() + .take(edit.range.end.line.try_into().unwrap()) + .map(|line| line.len() + '\n'.len_utf8()) + .sum::() + + >::try_into(edit.range.end.character).unwrap(); + text.replace_range(start..end, &edit.new_text); + } + text + } + #[test] fn test_lsp_document_formatting() { let uri = Uri::from_str("file:///home/documents/file.luau").unwrap(); @@ -420,17 +422,8 @@ mod tests { expect_server_initialized(&client.receiver, 1); let edits: Vec = expect_response(&client.receiver, 2); - assert_eq!(edits.len(), 1); - assert_eq!( - edits[0], - TextEdit { - range: Range { - start: Position::new(0, 0), - end: Position::new(0, 14), - }, - new_text: "local x = 1\n".to_string(), - } - ); + let formatted = with_edits(contents, edits); + assert_eq!(formatted, "local x = 1\n"); expect_server_shutdown(&client.receiver, 3); assert!(client.receiver.is_empty()); @@ -484,17 +477,8 @@ mod tests { expect_server_initialized(&client.receiver, 1); let edits: Vec = expect_response(&client.receiver, 2); - assert_eq!(edits.len(), 1); - assert_eq!( - edits[0], - TextEdit { - range: Range { - start: Position::new(0, 0), - end: Position::new(1, 18), - }, - new_text: "local x = 1\nlocal y = 2\n".to_string(), - } - ); + let formatted = with_edits(contents, edits); + assert_eq!(formatted, "local x = 1\nlocal y = 2\n"); expect_server_shutdown(&client.receiver, 3); assert!(client.receiver.is_empty()); From 39a6489c695a78bfbab2986b76b6b94e824cfbf6 Mon Sep 17 00:00:00 2001 From: Jonas Zeltner Date: Mon, 15 Sep 2025 20:52:16 +0200 Subject: [PATCH 4/8] Add missing range formatting capability --- src/cli/lsp.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cli/lsp.rs b/src/cli/lsp.rs index aab104d5..3c7e4b62 100644 --- a/src/cli/lsp.rs +++ b/src/cli/lsp.rs @@ -174,6 +174,7 @@ fn handle_request( fn main_loop(connection: Connection, config_resolver: &mut ConfigResolver) -> anyhow::Result<()> { let initialize_result = InitializeResult { capabilities: ServerCapabilities { + document_range_formatting_provider: Some(OneOf::Left(true)), document_formatting_provider: Some(OneOf::Left(true)), text_document_sync: Some(TextDocumentSyncCapability::Kind( TextDocumentSyncKind::INCREMENTAL, @@ -289,6 +290,7 @@ mod tests { && result == serde_json::json!({ "capabilities": ServerCapabilities { + document_range_formatting_provider: Some(OneOf::Left(true)), document_formatting_provider: Some(OneOf::Left(true)), text_document_sync: Some(TextDocumentSyncCapability::Kind( TextDocumentSyncKind::INCREMENTAL, From 2c7a1c0570eff470d8be7d8e4a919e397dc1b726 Mon Sep 17 00:00:00 2001 From: Jonas Zeltner Date: Mon, 15 Sep 2025 20:53:01 +0200 Subject: [PATCH 5/8] Use correct package name --- src/cli/lsp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/lsp.rs b/src/cli/lsp.rs index 3c7e4b62..56b85670 100644 --- a/src/cli/lsp.rs +++ b/src/cli/lsp.rs @@ -182,7 +182,7 @@ fn main_loop(connection: Connection, config_resolver: &mut ConfigResolver) -> an ..Default::default() }, server_info: Some(ServerInfo { - name: "stylua".to_string(), + name: env!("CARGO_PKG_NAME").to_string(), version: Some(env!("CARGO_PKG_VERSION").to_string()), }), }; @@ -298,7 +298,7 @@ mod tests { ..Default::default() }, "serverInfo": Some(ServerInfo { - name: "stylua".to_string(), + name: env!("CARGO_PKG_NAME").to_string(), version: Some(env!("CARGO_PKG_VERSION").to_string()), }), }) => {} From 7fc1d9a6bd455acb93de1859e7c85e422a064bdc Mon Sep 17 00:00:00 2001 From: Jonas Zeltner Date: Mon, 15 Sep 2025 21:15:30 +0200 Subject: [PATCH 6/8] Test exact edits returned by formatting handler --- src/cli/lsp.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/cli/lsp.rs b/src/cli/lsp.rs index 56b85670..f4c8fa99 100644 --- a/src/cli/lsp.rs +++ b/src/cli/lsp.rs @@ -348,7 +348,7 @@ mod tests { assert!(client.receiver.is_empty()); } - fn with_edits(text: &str, mut edits: Vec) -> String { + fn apply_text_edits_to(text: &str, mut edits: Vec) -> String { edits.sort_by(|a, b| match a.range.start.line.cmp(&b.range.start.line) { Ordering::Equal => a .range @@ -424,7 +424,28 @@ mod tests { expect_server_initialized(&client.receiver, 1); let edits: Vec = expect_response(&client.receiver, 2); - let formatted = with_edits(contents, edits); + assert_eq!( + edits, + [ + TextEdit { + range: Range::new(Position::new(0, 6), Position::new(0, 7)), + new_text: "".to_string() + }, + TextEdit { + range: Range::new(Position::new(0, 8), Position::new(0, 9)), + new_text: "".to_string() + }, + TextEdit { + range: Range::new(Position::new(0, 12), Position::new(0, 13)), + new_text: "".to_string() + }, + TextEdit { + range: Range::new(Position::new(0, 14), Position::new(0, 14)), + new_text: "\n".to_string() + }, + ] + ); + let formatted = apply_text_edits_to(contents, edits); assert_eq!(formatted, "local x = 1\n"); expect_server_shutdown(&client.receiver, 3); @@ -479,7 +500,36 @@ mod tests { expect_server_initialized(&client.receiver, 1); let edits: Vec = expect_response(&client.receiver, 2); - let formatted = with_edits(contents, edits); + assert_eq!( + edits, + [ + TextEdit { + range: Range::new(Position::new(1, 6), Position::new(1, 9)), + new_text: "".to_string() + }, + TextEdit { + range: Range::new(Position::new(1, 10), Position::new(1, 11)), + new_text: "".to_string() + }, + TextEdit { + range: Range::new(Position::new(1, 12), Position::new(1, 13)), + new_text: "".to_string() + }, + TextEdit { + range: Range::new(Position::new(1, 14), Position::new(1, 15)), + new_text: "".to_string() + }, + TextEdit { + range: Range::new(Position::new(1, 16), Position::new(1, 17)), + new_text: "".to_string() + }, + TextEdit { + range: Range::new(Position::new(1, 18), Position::new(1, 18)), + new_text: "\n".to_string() + }, + ] + ); + let formatted = apply_text_edits_to(contents, edits); assert_eq!(formatted, "local x = 1\nlocal y = 2\n"); expect_server_shutdown(&client.receiver, 3); From 1df14ccc4f9a5c719526d2d5a52a8638e591eb16 Mon Sep 17 00:00:00 2001 From: Jonas Zeltner Date: Mon, 15 Sep 2025 21:34:35 +0200 Subject: [PATCH 7/8] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0b1a75..a44047f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- In language server mode, compute the difference between the unformatted and formatted document and only respond with the changes. +- Include `serverInfo` in the language server's [`InitializeResponse`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initializeResult) + ### Fixed - Fixed comments lost from expression after parentheses are removed when we are attempting to "hang" the expression. ([#1033](https://github.com/JohnnyMorganz/StyLua/issues/1033)) +- `document_range_formatting_provider` field missing from `ServerCapabilities` ## [2.2.0] - 2025-09-14 From 33fe055c1da153eba8020f17c47f848d367a95db Mon Sep 17 00:00:00 2001 From: Jonas Zeltner Date: Sat, 20 Sep 2025 18:43:22 +0200 Subject: [PATCH 8/8] Add initialization option to respect editor's formatting options --- CHANGELOG.md | 5 +++++ README.md | 2 ++ src/cli/lsp.rs | 50 +++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a44047f4..35a0db3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- The language server has an initialization option called `respect_editor_formatting_options`. + If it's true, the formatting handler will override the configurations `indent-width` and `indent-type` with values from [FormattingOptions](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#formattingOptions) + ### Changed - In language server mode, compute the difference between the unformatted and formatted document and only respond with the changes. diff --git a/README.md b/README.md index e658a8ae..9c2873b9 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,8 @@ StyLua can run as a language server, connecting with language clients that follo It will then respond to `textDocument/formatting` and `textDocument/rangeFormatting` requests. Formatting is only performed on files with a `lua` or `luau` language ID. +If the initialization option `respect_editor_formatting_options` is set to `true`, the formatting handler will override the configurations `indent-width` and `indent-type` with values from [FormattingOptions](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#formattingOptions). + You can start the language server by running: ```sh diff --git a/src/cli/lsp.rs b/src/cli/lsp.rs index f4c8fa99..0fd1abf5 100644 --- a/src/cli/lsp.rs +++ b/src/cli/lsp.rs @@ -4,12 +4,13 @@ use lsp_server::{Connection, ErrorCode, Message, Response}; use lsp_textdocument::{FullTextDocument, TextDocuments}; use lsp_types::{ request::{Formatting, RangeFormatting, Request}, - DocumentFormattingParams, DocumentRangeFormattingParams, InitializeResult, OneOf, Range, - ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, - Uri, + DocumentFormattingParams, DocumentRangeFormattingParams, FormattingOptions, InitializeParams, + InitializeResult, OneOf, Range, ServerCapabilities, ServerInfo, TextDocumentSyncCapability, + TextDocumentSyncKind, TextEdit, Uri, }; +use serde::Deserialize; use similar::{DiffOp, TextDiff}; -use stylua_lib::{format_code, OutputVerification}; +use stylua_lib::{format_code, IndentType, OutputVerification}; use crate::{config::ConfigResolver, opt}; @@ -64,6 +65,7 @@ fn handle_formatting( document: &FullTextDocument, range: Option, config_resolver: &mut ConfigResolver, + formatting_options: Option<&FormattingOptions>, ) -> Option> { if document.language_id() != "lua" && document.language_id() != "luau" { return None; @@ -71,10 +73,22 @@ fn handle_formatting( let contents = document.get_content(None); - let config = config_resolver + let mut config = config_resolver .load_configuration(uri.path().as_str().as_ref()) .unwrap_or_default(); + if let Some(formatting_options) = formatting_options { + config.indent_width = formatting_options + .tab_size + .try_into() + .expect("u32 fits into usize"); + config.indent_type = if formatting_options.insert_spaces { + IndentType::Spaces + } else { + IndentType::Tabs + }; + } + let formatted_contents = format_code(contents, config, range, OutputVerification::None).ok()?; let operations = @@ -94,6 +108,7 @@ fn handle_request( request: lsp_server::Request, documents: &TextDocuments, config_resolver: &mut ConfigResolver, + respect_editor_formatting_options: bool, ) -> Response { match request.method.as_str() { Formatting::METHOD => { @@ -115,6 +130,7 @@ fn handle_request( document, None, config_resolver, + respect_editor_formatting_options.then_some(¶ms.options), ) { Some(edits) => Response::new_ok(request.id, edits), None => Response::new_ok(request.id, serde_json::Value::Null), @@ -151,6 +167,7 @@ fn handle_request( document, Some(range), config_resolver, + respect_editor_formatting_options.then_some(¶ms.options), ) { Some(edits) => Response::new_ok(request.id, edits), None => Response::new_ok(request.id, serde_json::Value::Null), @@ -171,6 +188,12 @@ fn handle_request( } } +#[derive(Deserialize, Default)] +#[serde(default)] +struct InitializationOptions { + respect_editor_formatting_options: Option, +} + fn main_loop(connection: Connection, config_resolver: &mut ConfigResolver) -> anyhow::Result<()> { let initialize_result = InitializeResult { capabilities: ServerCapabilities { @@ -187,7 +210,15 @@ fn main_loop(connection: Connection, config_resolver: &mut ConfigResolver) -> an }), }; - let (id, _) = connection.initialize_start()?; + let (id, initialize_params) = connection.initialize_start()?; + + let initialize_params = serde_json::from_value::(initialize_params)?; + let respect_editor_formatting_options = initialize_params + .initialization_options + .and_then(|opt| serde_json::from_value::(opt).ok()) + .and_then(|opt| opt.respect_editor_formatting_options) + .unwrap_or_default(); + connection.initialize_finish(id, serde_json::to_value(initialize_result)?)?; let mut documents = TextDocuments::new(); @@ -198,7 +229,12 @@ fn main_loop(connection: Connection, config_resolver: &mut ConfigResolver) -> an break; } - let response = handle_request(req, &documents, config_resolver); + let response = handle_request( + req, + &documents, + config_resolver, + respect_editor_formatting_options, + ); connection.sender.send(Message::Response(response))? } Message::Response(_) => {}