lsp: Support textDocument/formatting request

This commit is contained in:
Waqar Ahmed 2024-02-05 22:42:22 +05:00 committed by Olivier Goffart
parent d769f272e1
commit 70fe0df97f
3 changed files with 137 additions and 2 deletions

View file

@ -87,6 +87,7 @@ lsp-types = { version = "0.95.0", features = ["proposed"] }
rowan = "0.15.5"
serde = "1.0.118"
serde_json = "1.0.60"
dissimilar = "1.0.7"
# for the preview-engine feature
i-slint-backend-selector = { workspace = true, features = ["default"], optional = true }
@ -95,6 +96,9 @@ i-slint-core = { workspace = true, features = ["std"], optional = true }
slint = { workspace = true, features = ["compat-1-2"], optional = true }
slint-interpreter = { workspace = true, features = ["compat-1-2", "highlight", "internal"], optional = true }
# for formatting support in lsp
slint-fmt = { path = "../fmt" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
clap = { version = "4.0", features = ["derive", "wrap_help"] }
crossbeam-channel = "0.5" # must match the version used by lsp-server

View file

@ -5,6 +5,7 @@
mod completion;
mod component_catalog;
mod formatting;
mod goto;
mod properties;
mod semantic_tokens;
@ -29,8 +30,8 @@ use i_slint_compiler::{
use i_slint_compiler::{typeloader::TypeLoader, typeregister::TypeRegister};
use lsp_types::request::{
CodeActionRequest, CodeLensRequest, ColorPresentationRequest, Completion, DocumentColor,
DocumentHighlightRequest, DocumentSymbolRequest, ExecuteCommand, GotoDefinition, HoverRequest,
PrepareRenameRequest, Rename, SemanticTokensFullRequest,
DocumentHighlightRequest, DocumentSymbolRequest, ExecuteCommand, Formatting, GotoDefinition,
HoverRequest, PrepareRenameRequest, Rename, SemanticTokensFullRequest,
};
use lsp_types::{
ClientCapabilities, CodeActionOrCommand, CodeActionProviderCapability, CodeLens,
@ -228,6 +229,7 @@ pub fn server_initialize_result(client_cap: &ClientCapabilities) -> InitializeRe
OneOf::Left(true)
},
),
document_formatting_provider: Some(OneOf::Left(true)),
..ServerCapabilities::default()
},
server_info: Some(ServerInfo {
@ -423,6 +425,10 @@ pub fn register_request_handlers(rh: &mut RequestHandler) {
};
Ok(None)
});
rh.register::<Formatting, _>(|params, ctx| async move {
let document_cache = ctx.document_cache.borrow_mut();
Ok(formatting::format_document(params, &document_cache))
});
}
#[cfg(any(feature = "preview-builtin", feature = "preview-external"))]

View file

@ -0,0 +1,125 @@
use super::DocumentCache;
use crate::util::map_range;
use dissimilar::Chunk;
use i_slint_compiler::parser::SyntaxToken;
use lsp_types::{DocumentFormattingParams, TextEdit};
use rowan::{TextRange, TextSize};
struct StringWriter {
text: String,
}
impl slint_fmt::writer::TokenWriter for StringWriter {
fn no_change(&mut self, token: SyntaxToken) -> std::io::Result<()> {
self.text += &token.text();
Ok(())
}
fn with_new_content(&mut self, _token: SyntaxToken, contents: &str) -> std::io::Result<()> {
self.text += contents;
Ok(())
}
fn insert_before(&mut self, token: SyntaxToken, contents: &str) -> std::io::Result<()> {
self.text += contents;
self.text += &token.text();
Ok(())
}
}
pub fn format_document(
params: DocumentFormattingParams,
document_cache: &DocumentCache,
) -> Option<Vec<TextEdit>> {
let file_path = super::uri_to_file(&params.text_document.uri)?;
let doc = document_cache.documents.get_document(&file_path)?;
let doc = doc.node.as_ref()?;
let mut writer = StringWriter { text: String::new() };
slint_fmt::fmt::format_document(doc.clone(), &mut writer).ok()?;
let original: String = doc.text().into();
let diff = dissimilar::diff(&original, &writer.text);
let mut pos = TextSize::default();
let mut last_was_deleted = false;
let mut edits: Vec<TextEdit> = Vec::new();
for d in diff {
match d {
Chunk::Equal(text) => {
last_was_deleted = false;
pos += TextSize::of(text)
}
Chunk::Delete(text) => {
let len = TextSize::of(text);
let deleted_range = map_range(&doc.source_file, TextRange::at(pos, len));
edits.push(TextEdit { range: deleted_range, new_text: String::new() });
last_was_deleted = true;
pos += len;
}
Chunk::Insert(text) => {
if last_was_deleted {
// if last was deleted, then this is a replace
edits.last_mut().unwrap().new_text = text.into();
last_was_deleted = false;
continue;
}
let range = TextRange::empty(pos);
let range = map_range(&doc.source_file, range);
edits.push(TextEdit { range, new_text: text.into() });
}
}
}
Some(edits)
}
#[cfg(test)]
mod tests {
use super::*;
use lsp_types::{Position, Range, TextEdit};
/// Given an unformatted source text, return text edits that will turn the source into formatted text
fn get_formatting_edits(source: &str) -> Option<Vec<TextEdit>> {
let (dc, uri, _) = crate::language::test::loaded_document_cache(source.into());
// we only care about "uri" in params
let params = lsp_types::DocumentFormattingParams {
text_document: lsp_types::TextDocumentIdentifier { uri },
options: lsp_types::FormattingOptions::default(),
work_done_progress_params: lsp_types::WorkDoneProgressParams::default(),
};
format_document(params, &dc)
}
#[test]
fn test_formatting() {
let edits = get_formatting_edits(
"component Bar inherits Text { nope := Rectangle {} property <string> red; }",
)
.unwrap();
macro_rules! text_edit {
($start_line:literal, $start_col:literal, $end_line:literal, $end_col:literal, $text:literal) => {
TextEdit {
range: Range {
start: Position { line: $start_line, character: $start_col },
end: Position { line: $end_line, character: $end_col },
},
new_text: $text.into(),
}
};
}
let expected = vec![
text_edit!(0, 29, 0, 29, "\n "),
text_edit!(0, 49, 0, 50, " }\n\n "),
text_edit!(0, 73, 0, 75, "\n}\n"),
];
assert_eq!(edits.len(), expected.len());
for (actual, expected) in edits.iter().zip(expected.iter()) {
assert_eq!(actual, expected);
}
}
}