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 |
3 | use super::DocumentCache; |
4 | use crate::fmt::{fmt, writer}; |
5 | use crate::util::map_range; |
6 | use dissimilar::Chunk; |
7 | use i_slint_compiler::parser::SyntaxToken; |
8 | use lsp_types::{DocumentFormattingParams, TextEdit}; |
9 | use rowan::{TextRange, TextSize}; |
10 | |
11 | struct StringWriter { |
12 | text: String, |
13 | } |
14 | |
15 | impl 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 | |
33 | pub fn format_document( |
34 | params: DocumentFormattingParams, |
35 | document_cache: &DocumentCache, |
36 | ) -> Option<Vec<TextEdit>> { |
37 | let file_path = super::uri_to_file(¶ms.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)] |
82 | mod 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 | |