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 | |
4 | //! Data structures common between LSP and previewer |
5 | |
6 | use i_slint_compiler::diagnostics::{SourceFile, SourceFileVersion}; |
7 | use i_slint_compiler::object_tree::ElementRc; |
8 | use lsp_types::{TextEdit, Url, WorkspaceEdit}; |
9 | |
10 | use std::{collections::HashMap, path::PathBuf}; |
11 | |
12 | pub type Error = Box<dyn std::error::Error>; |
13 | pub type Result<T> = std::result::Result<T, Error>; |
14 | pub type UrlVersion = Option<i32>; |
15 | |
16 | #[cfg (target_arch = "wasm32" )] |
17 | use crate::wasm_prelude::*; |
18 | |
19 | #[derive (Clone)] |
20 | pub struct ElementRcNode { |
21 | pub element: ElementRc, |
22 | pub debug_index: usize, |
23 | } |
24 | |
25 | impl std::fmt::Debug for ElementRcNode { |
26 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
27 | let (path: PathBuf, offset: u32) = self.path_and_offset(); |
28 | write!(f, "ElementNode {{ {path:?}: {offset} }}" ) |
29 | } |
30 | } |
31 | |
32 | impl ElementRcNode { |
33 | pub fn find_in(element: ElementRc, path: &std::path::Path, offset: u32) -> Option<Self> { |
34 | let debug_index = element.borrow().debug.iter().position(|(n, _)| { |
35 | u32::from(n.text_range().start()) == offset && n.source_file.path() == path |
36 | })?; |
37 | |
38 | Some(Self { element, debug_index }) |
39 | } |
40 | |
41 | pub fn with_element_debug<R>( |
42 | &self, |
43 | func: impl Fn( |
44 | &i_slint_compiler::parser::syntax_nodes::Element, |
45 | &Option<i_slint_compiler::layout::Layout>, |
46 | ) -> R, |
47 | ) -> R { |
48 | let elem = self.element.borrow(); |
49 | let (n, l) = &elem.debug.get(self.debug_index).unwrap(); |
50 | func(n, l) |
51 | } |
52 | |
53 | pub fn with_element_node<R>( |
54 | &self, |
55 | func: impl Fn(&i_slint_compiler::parser::syntax_nodes::Element) -> R, |
56 | ) -> R { |
57 | let elem = self.element.borrow(); |
58 | func(&elem.debug.get(self.debug_index).unwrap().0) |
59 | } |
60 | |
61 | pub fn path_and_offset(&self) -> (PathBuf, u32) { |
62 | self.with_element_node(|n| { |
63 | (n.source_file.path().to_owned(), u32::from(n.text_range().start())) |
64 | }) |
65 | } |
66 | |
67 | pub fn is_layout(&self) -> bool { |
68 | self.with_element_debug(|_, l| l.is_some()) |
69 | } |
70 | } |
71 | |
72 | pub fn create_workspace_edit( |
73 | uri: Url, |
74 | version: SourceFileVersion, |
75 | edits: Vec<TextEdit>, |
76 | ) -> WorkspaceEdit { |
77 | let edits: Vec> = editsimpl Iterator- >
|
78 | .into_iter() |
79 | .map(lsp_types::OneOf::Left::<TextEdit, lsp_types::AnnotatedTextEdit>) |
80 | .collect(); |
81 | let edit: TextDocumentEdit = lsp_types::TextDocumentEdit { |
82 | text_document: lsp_types::OptionalVersionedTextDocumentIdentifier { uri, version }, |
83 | edits, |
84 | }; |
85 | let changes: DocumentChanges = lsp_types::DocumentChanges::Edits(vec![edit]); |
86 | WorkspaceEdit { document_changes: Some(changes), ..Default::default() } |
87 | } |
88 | |
89 | pub fn create_workspace_edit_from_source_file( |
90 | source_file: &SourceFile, |
91 | edits: Vec<TextEdit>, |
92 | ) -> Option<WorkspaceEdit> { |
93 | Some(create_workspace_edit( |
94 | uri:Url::from_file_path(source_file.path()).ok()?, |
95 | source_file.version(), |
96 | edits, |
97 | )) |
98 | } |
99 | |
100 | /// A versioned file |
101 | #[derive (Clone, serde::Deserialize, serde::Serialize, PartialEq, Eq)] |
102 | pub struct VersionedUrl { |
103 | /// The file url |
104 | url: Url, |
105 | // The file version |
106 | version: UrlVersion, |
107 | } |
108 | |
109 | impl VersionedUrl { |
110 | pub fn new(url: Url, version: UrlVersion) -> Self { |
111 | VersionedUrl { url, version } |
112 | } |
113 | |
114 | pub fn url(&self) -> &Url { |
115 | &self.url |
116 | } |
117 | |
118 | pub fn version(&self) -> &UrlVersion { |
119 | &self.version |
120 | } |
121 | } |
122 | |
123 | impl std::fmt::Debug for VersionedUrl { |
124 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
125 | let version: String = self.version.map(|v: i32| format!("v {v}" )).unwrap_or_else(|| "none" .to_string()); |
126 | write!(f, " {}@ {}" , self.url, version) |
127 | } |
128 | } |
129 | |
130 | /// A versioned file |
131 | #[derive (Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] |
132 | pub struct Position { |
133 | /// The file url |
134 | pub url: Url, |
135 | /// The offset in the file pointed to by the `url` |
136 | pub offset: u32, |
137 | } |
138 | |
139 | /// A versioned file |
140 | #[derive (Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] |
141 | pub struct VersionedPosition { |
142 | /// The file url |
143 | url: VersionedUrl, |
144 | /// The offset in the file pointed to by the `url` |
145 | offset: u32, |
146 | } |
147 | |
148 | #[allow (unused)] |
149 | impl VersionedPosition { |
150 | pub fn new(url: VersionedUrl, offset: u32) -> Self { |
151 | VersionedPosition { url, offset } |
152 | } |
153 | |
154 | pub fn url(&self) -> &Url { |
155 | self.url.url() |
156 | } |
157 | |
158 | pub fn version(&self) -> &UrlVersion { |
159 | self.url.version() |
160 | } |
161 | |
162 | pub fn offset(&self) -> u32 { |
163 | self.offset |
164 | } |
165 | } |
166 | |
167 | #[derive (Default, Clone, PartialEq, Debug, serde::Deserialize, serde::Serialize)] |
168 | pub struct PreviewConfig { |
169 | pub hide_ui: Option<bool>, |
170 | pub style: String, |
171 | pub include_paths: Vec<PathBuf>, |
172 | pub library_paths: HashMap<String, PathBuf>, |
173 | } |
174 | |
175 | /// The Component to preview |
176 | #[allow (unused)] |
177 | #[derive (Clone, Debug, serde::Deserialize, serde::Serialize)] |
178 | pub struct PreviewComponent { |
179 | /// The file name to preview |
180 | pub url: Url, |
181 | /// The name of the component within that file. |
182 | /// If None, then the last component is going to be shown. |
183 | pub component: Option<String>, |
184 | |
185 | /// The style name for the preview |
186 | pub style: String, |
187 | } |
188 | |
189 | #[allow (unused)] |
190 | #[derive (Clone, Debug, serde::Deserialize, serde::Serialize)] |
191 | pub enum LspToPreviewMessage { |
192 | SetContents { url: VersionedUrl, contents: String }, |
193 | SetConfiguration { config: PreviewConfig }, |
194 | ShowPreview(PreviewComponent), |
195 | HighlightFromEditor { url: Option<Url>, offset: u32 }, |
196 | KnownComponents { url: Option<VersionedUrl>, components: Vec<ComponentInformation> }, |
197 | } |
198 | |
199 | #[allow (unused)] |
200 | #[derive (Clone, Debug, serde::Deserialize, serde::Serialize)] |
201 | pub struct Diagnostic { |
202 | pub message: String, |
203 | pub file: Option<String>, |
204 | pub line: usize, |
205 | pub column: usize, |
206 | pub level: String, |
207 | } |
208 | |
209 | #[allow (unused)] |
210 | #[derive (Clone, Eq, Debug, PartialEq, serde::Deserialize, serde::Serialize)] |
211 | pub struct PropertyChange { |
212 | pub name: String, |
213 | pub value: String, |
214 | } |
215 | |
216 | impl PropertyChange { |
217 | #[allow (unused)] |
218 | pub fn new(name: &str, value: String) -> Self { |
219 | PropertyChange { name: name.to_string(), value } |
220 | } |
221 | } |
222 | |
223 | #[allow (unused)] |
224 | #[derive (Clone, Debug, serde::Deserialize, serde::Serialize)] |
225 | pub enum PreviewToLspMessage { |
226 | /// Show a status message in the editor |
227 | Status { message: String, health: crate::lsp_ext::Health }, |
228 | /// Report diagnostics to editor. |
229 | Diagnostics { uri: Url, diagnostics: Vec<lsp_types::Diagnostic> }, |
230 | /// Show a document in the editor. |
231 | ShowDocument { file: Url, selection: lsp_types::Range }, |
232 | /// Switch between native and WASM preview (if supported) |
233 | PreviewTypeChanged { is_external: bool }, |
234 | /// Request all documents and configuration to be sent from the LSP to the |
235 | /// Preview. |
236 | RequestState { unused: bool }, |
237 | /// Update properties on an element at `position` |
238 | /// The LSP side needs to look at properties: It sees way more of them! |
239 | UpdateElement { |
240 | label: Option<String>, |
241 | position: VersionedPosition, |
242 | properties: Vec<PropertyChange>, |
243 | }, |
244 | /// Pass a `WorkspaceEdit` on to the editor |
245 | SendWorkspaceEdit { label: Option<String>, edit: lsp_types::WorkspaceEdit }, |
246 | } |
247 | |
248 | /// Information on the Element types available |
249 | #[derive (Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)] |
250 | pub struct ComponentInformation { |
251 | /// The name of the type |
252 | pub name: String, |
253 | /// A broad category to group types by |
254 | pub category: String, |
255 | /// This type is a global component |
256 | pub is_global: bool, |
257 | /// This type is built into Slint |
258 | pub is_builtin: bool, |
259 | /// This type is a standard widget |
260 | pub is_std_widget: bool, |
261 | /// This type was exported |
262 | pub is_exported: bool, |
263 | /// This is a layout |
264 | pub is_layout: bool, |
265 | /// This element fills its parent |
266 | pub fills_parent: bool, |
267 | /// The URL to the file containing this type |
268 | pub defined_at: Option<Position>, |
269 | /// Default property values |
270 | pub default_properties: Vec<PropertyChange>, |
271 | } |
272 | |
273 | impl ComponentInformation { |
274 | pub fn import_file_name(&self, current_uri: &Option<lsp_types::Url>) -> Option<String> { |
275 | if self.is_std_widget { |
276 | Some("std-widgets.slint" .to_string()) |
277 | } else { |
278 | let url: &Url = self.defined_at.as_ref().map(|p: &Position| &p.url)?; |
279 | if let Some(current_uri: &Url) = current_uri { |
280 | lsp_types::Url::make_relative(self:current_uri, url) |
281 | } else { |
282 | url.to_file_path().ok().map(|p: PathBuf| p.to_string_lossy().to_string()) |
283 | } |
284 | } |
285 | } |
286 | } |
287 | |
288 | #[cfg (any(feature = "preview-external" , feature = "preview-engine" ))] |
289 | pub mod lsp_to_editor { |
290 | use lsp_types::notification::Notification; |
291 | |
292 | pub fn send_status_notification( |
293 | sender: &crate::ServerNotifier, |
294 | message: &str, |
295 | health: crate::lsp_ext::Health, |
296 | ) { |
297 | sender |
298 | .send_notification( |
299 | crate::lsp_ext::ServerStatusNotification::METHOD.into(), |
300 | crate::lsp_ext::ServerStatusParams { |
301 | health, |
302 | quiescent: false, |
303 | message: Some(message.into()), |
304 | }, |
305 | ) |
306 | .unwrap_or_else(|e| eprintln!("Error sending notification: {:?}" , e)); |
307 | } |
308 | |
309 | pub fn notify_lsp_diagnostics( |
310 | sender: &crate::ServerNotifier, |
311 | uri: lsp_types::Url, |
312 | diagnostics: Vec<lsp_types::Diagnostic>, |
313 | ) -> Option<()> { |
314 | sender |
315 | .send_notification( |
316 | "textDocument/publishDiagnostics" .into(), |
317 | lsp_types::PublishDiagnosticsParams { uri, diagnostics, version: None }, |
318 | ) |
319 | .ok() |
320 | } |
321 | |
322 | fn show_document_request_from_element_callback( |
323 | uri: lsp_types::Url, |
324 | range: lsp_types::Range, |
325 | ) -> Option<lsp_types::ShowDocumentParams> { |
326 | if range.start.character == 0 || range.end.character == 0 { |
327 | return None; |
328 | } |
329 | |
330 | Some(lsp_types::ShowDocumentParams { |
331 | uri, |
332 | external: Some(false), |
333 | take_focus: Some(true), |
334 | selection: Some(range), |
335 | }) |
336 | } |
337 | |
338 | pub async fn send_show_document_to_editor( |
339 | sender: crate::ServerNotifier, |
340 | file: lsp_types::Url, |
341 | range: lsp_types::Range, |
342 | ) { |
343 | let Some(params) = show_document_request_from_element_callback(file, range) else { |
344 | return; |
345 | }; |
346 | let Ok(fut) = sender.send_request::<lsp_types::request::ShowDocument>(params) else { |
347 | return; |
348 | }; |
349 | |
350 | let _ = fut.await; |
351 | } |
352 | } |
353 | |