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
6use i_slint_compiler::diagnostics::{SourceFile, SourceFileVersion};
7use i_slint_compiler::object_tree::ElementRc;
8use lsp_types::{TextEdit, Url, WorkspaceEdit};
9
10use std::{collections::HashMap, path::PathBuf};
11
12pub type Error = Box<dyn std::error::Error>;
13pub type Result<T> = std::result::Result<T, Error>;
14pub type UrlVersion = Option<i32>;
15
16#[cfg(target_arch = "wasm32")]
17use crate::wasm_prelude::*;
18
19#[derive(Clone)]
20pub struct ElementRcNode {
21 pub element: ElementRc,
22 pub debug_index: usize,
23}
24
25impl 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
32impl 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
72pub 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
89pub 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)]
102pub struct VersionedUrl {
103 /// The file url
104 url: Url,
105 // The file version
106 version: UrlVersion,
107}
108
109impl 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
123impl 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)]
132pub 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)]
141pub 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)]
149impl 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)]
168pub 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)]
178pub 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)]
191pub 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)]
201pub 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)]
211pub struct PropertyChange {
212 pub name: String,
213 pub value: String,
214}
215
216impl 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)]
225pub 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)]
250pub 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
273impl 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"))]
289pub 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