mirror of
https://github.com/slint-ui/slint.git
synced 2025-11-02 21:03:00 +00:00
lsp: Support textDocument/formatting request
This commit is contained in:
parent
d769f272e1
commit
70fe0df97f
3 changed files with 137 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"))]
|
||||
|
|
|
|||
125
tools/lsp/language/formatting.rs
Normal file
125
tools/lsp/language/formatting.rs
Normal 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(¶ms.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue