1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
3use super::DocumentCache;
4use crate::fmt::{fmt, writer};
5use crate::util::map_range;
6use dissimilar::Chunk;
7use i_slint_compiler::parser::SyntaxToken;
8use lsp_types::{DocumentFormattingParams, TextEdit};
9use rowan::{TextRange, TextSize};
10
11struct StringWriter {
12 text: String,
13}
14
15impl writer::TokenWriter for StringWriter {
16 fn no_change(&mut self, token: SyntaxToken) -> std::io::Result<()> {
17 self.text += token.text();
18 Ok(())
19 }
20
21 fn with_new_content(&mut self, _token: SyntaxToken, contents: &str) -> std::io::Result<()> {
22 self.text += contents;
23 Ok(())
24 }
25
26 fn insert_before(&mut self, token: SyntaxToken, contents: &str) -> std::io::Result<()> {
27 self.text += contents;
28 self.text += token.text();
29 Ok(())
30 }
31}
32
33pub fn format_document(
34 params: DocumentFormattingParams,
35 document_cache: &DocumentCache,
36) -> Option<Vec<TextEdit>> {
37 let file_path = super::uri_to_file(&params.text_document.uri)?;
38 let doc = document_cache.documents.get_document(&file_path)?;
39 let doc = doc.node.as_ref()?;
40
41 let mut writer = StringWriter { text: String::new() };
42 fmt::format_document(doc.clone(), &mut writer).ok()?;
43
44 let original: String = doc.text().into();
45 let diff = dissimilar::diff(&original, &writer.text);
46
47 let mut pos = TextSize::default();
48 let mut last_was_deleted = false;
49 let mut edits: Vec<TextEdit> = Vec::new();
50
51 for d in diff {
52 match d {
53 Chunk::Equal(text) => {
54 last_was_deleted = false;
55 pos += TextSize::of(text)
56 }
57 Chunk::Delete(text) => {
58 let len = TextSize::of(text);
59 let deleted_range = map_range(&doc.source_file, TextRange::at(pos, len));
60 edits.push(TextEdit { range: deleted_range, new_text: String::new() });
61 last_was_deleted = true;
62 pos += len;
63 }
64 Chunk::Insert(text) => {
65 if last_was_deleted {
66 // if last was deleted, then this is a replace
67 edits.last_mut().unwrap().new_text = text.into();
68 last_was_deleted = false;
69 continue;
70 }
71
72 let range = TextRange::empty(pos);
73 let range = map_range(&doc.source_file, range);
74 edits.push(TextEdit { range, new_text: text.into() });
75 }
76 }
77 }
78 Some(edits)
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use lsp_types::{Position, Range};
85
86 /// Given an unformatted source text, return text edits that will turn the source into formatted text
87 fn get_formatting_edits(source: &str) -> Option<Vec<TextEdit>> {
88 let (dc, uri, _) = crate::language::test::loaded_document_cache(source.into());
89 // we only care about "uri" in params
90 let params = lsp_types::DocumentFormattingParams {
91 text_document: lsp_types::TextDocumentIdentifier { uri },
92 options: lsp_types::FormattingOptions::default(),
93 work_done_progress_params: lsp_types::WorkDoneProgressParams::default(),
94 };
95 format_document(params, &dc)
96 }
97
98 #[test]
99 fn test_formatting() {
100 let edits = get_formatting_edits(
101 "component Bar inherits Text { nope := Rectangle {} property <string> red; }",
102 )
103 .unwrap();
104
105 macro_rules! text_edit {
106 ($start_line:literal, $start_col:literal, $end_line:literal, $end_col:literal, $text:literal) => {
107 TextEdit {
108 range: Range {
109 start: Position { line: $start_line, character: $start_col },
110 end: Position { line: $end_line, character: $end_col },
111 },
112 new_text: $text.into(),
113 }
114 };
115 }
116
117 let expected = [
118 text_edit!(0, 29, 0, 29, "\n "),
119 text_edit!(0, 49, 0, 50, " }\n\n "),
120 text_edit!(0, 73, 0, 75, "\n}\n"),
121 ];
122
123 assert_eq!(edits.len(), expected.len());
124 for (actual, expected) in edits.iter().zip(expected.iter()) {
125 assert_eq!(actual, expected);
126 }
127 }
128}
129