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 | // cSpell: ignore descr rfind unindented |
5 | |
6 | pub mod completion; |
7 | mod component_catalog; |
8 | mod formatting; |
9 | mod goto; |
10 | pub mod properties; |
11 | mod semantic_tokens; |
12 | #[cfg (test)] |
13 | pub mod test; |
14 | |
15 | use crate::common::{self, Result}; |
16 | use crate::util; |
17 | |
18 | #[cfg (target_arch = "wasm32" )] |
19 | use crate::wasm_prelude::*; |
20 | use i_slint_compiler::object_tree::ElementRc; |
21 | use i_slint_compiler::parser::{syntax_nodes, NodeOrToken, SyntaxKind, SyntaxNode, SyntaxToken}; |
22 | use i_slint_compiler::pathutils::clean_path; |
23 | use i_slint_compiler::CompilerConfiguration; |
24 | use i_slint_compiler::{ |
25 | diagnostics::{BuildDiagnostics, SourceFileVersion}, |
26 | langtype::Type, |
27 | }; |
28 | use i_slint_compiler::{typeloader::TypeLoader, typeregister::TypeRegister}; |
29 | use lsp_types::request::{ |
30 | CodeActionRequest, CodeLensRequest, ColorPresentationRequest, Completion, DocumentColor, |
31 | DocumentHighlightRequest, DocumentSymbolRequest, ExecuteCommand, Formatting, GotoDefinition, |
32 | HoverRequest, PrepareRenameRequest, Rename, SemanticTokensFullRequest, |
33 | }; |
34 | use lsp_types::{ |
35 | ClientCapabilities, CodeActionOrCommand, CodeActionProviderCapability, CodeLens, |
36 | CodeLensOptions, Color, ColorInformation, ColorPresentation, Command, CompletionOptions, |
37 | DocumentSymbol, DocumentSymbolResponse, Hover, InitializeParams, InitializeResult, OneOf, |
38 | Position, PrepareRenameResponse, PublishDiagnosticsParams, RenameOptions, |
39 | SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, ServerCapabilities, |
40 | ServerInfo, TextDocumentSyncCapability, TextEdit, Url, WorkDoneProgressOptions, |
41 | }; |
42 | use std::cell::RefCell; |
43 | use std::collections::HashMap; |
44 | use std::future::Future; |
45 | use std::path::PathBuf; |
46 | use std::pin::Pin; |
47 | use std::rc::Rc; |
48 | |
49 | const QUERY_PROPERTIES_COMMAND: &str = "slint/queryProperties" ; |
50 | const REMOVE_BINDING_COMMAND: &str = "slint/removeBinding" ; |
51 | const SHOW_PREVIEW_COMMAND: &str = "slint/showPreview" ; |
52 | const SET_BINDING_COMMAND: &str = "slint/setBinding" ; |
53 | |
54 | pub fn uri_to_file(uri: &lsp_types::Url) -> Option<PathBuf> { |
55 | let path: PathBuf = uri.to_file_path().ok()?; |
56 | let cleaned_path: PathBuf = clean_path(&path); |
57 | Some(cleaned_path) |
58 | } |
59 | |
60 | fn command_list() -> Vec<String> { |
61 | vec![ |
62 | QUERY_PROPERTIES_COMMAND.into(), |
63 | REMOVE_BINDING_COMMAND.into(), |
64 | #[cfg (any(feature = "preview-builtin" , feature = "preview-external" ))] |
65 | SHOW_PREVIEW_COMMAND.into(), |
66 | SET_BINDING_COMMAND.into(), |
67 | ] |
68 | } |
69 | |
70 | fn create_show_preview_command( |
71 | pretty: bool, |
72 | file: &lsp_types::Url, |
73 | component_name: &str, |
74 | ) -> Command { |
75 | let title: String = format!(" {}Show Preview" , if pretty { &"▶ " } else { &"" }); |
76 | Command::new( |
77 | title, |
78 | SHOW_PREVIEW_COMMAND.into(), |
79 | arguments:Some(vec![file.as_str().into(), component_name.into()]), |
80 | ) |
81 | } |
82 | |
83 | #[cfg (any(feature = "preview-external" , feature = "preview-engine" ))] |
84 | pub fn request_state(ctx: &std::rc::Rc<Context>) { |
85 | let cache = ctx.document_cache.borrow(); |
86 | let documents = &cache.documents; |
87 | |
88 | for (p, d) in documents.all_file_documents() { |
89 | if let Some(node) = &d.node { |
90 | if p.starts_with("builtin:/" ) { |
91 | continue; // The preview knows these, too. |
92 | } |
93 | let Ok(url) = Url::from_file_path(p) else { |
94 | continue; |
95 | }; |
96 | ctx.server_notifier.send_message_to_preview(common::LspToPreviewMessage::SetContents { |
97 | url: common::VersionedUrl::new(url, node.source_file.version()), |
98 | contents: node.text().to_string(), |
99 | }) |
100 | } |
101 | } |
102 | ctx.server_notifier.send_message_to_preview(common::LspToPreviewMessage::SetConfiguration { |
103 | config: cache.preview_config.clone(), |
104 | }); |
105 | if let Some(c) = ctx.to_show.borrow().clone() { |
106 | ctx.server_notifier.send_message_to_preview(common::LspToPreviewMessage::ShowPreview(c)) |
107 | } |
108 | } |
109 | |
110 | /// A cache of loaded documents |
111 | pub struct DocumentCache { |
112 | pub(crate) documents: TypeLoader, |
113 | preview_config: common::PreviewConfig, |
114 | } |
115 | |
116 | impl DocumentCache { |
117 | pub fn new(config: CompilerConfiguration) -> Self { |
118 | let documents: TypeLoader = |
119 | TypeLoader::new(global_type_registry:TypeRegister::builtin(), compiler_config:config, &mut BuildDiagnostics::default()); |
120 | Self { documents, preview_config: Default::default() } |
121 | } |
122 | |
123 | pub fn document_version(&self, target_uri: &lsp_types::Url) -> SourceFileVersion { |
124 | self.documents |
125 | .get_document(&uri_to_file(target_uri).unwrap_or_default()) |
126 | .and_then(|doc: &Document| doc.node.as_ref()?.source_file.version()) |
127 | } |
128 | } |
129 | |
130 | pub struct Context { |
131 | pub document_cache: RefCell<DocumentCache>, |
132 | pub server_notifier: crate::ServerNotifier, |
133 | pub init_param: InitializeParams, |
134 | /// The last component for which the user clicked "show preview" |
135 | pub to_show: RefCell<Option<common::PreviewComponent>>, |
136 | } |
137 | |
138 | #[derive (Default)] |
139 | pub struct RequestHandler( |
140 | pub HashMap< |
141 | &'static str, |
142 | Box< |
143 | dyn Fn( |
144 | serde_json::Value, |
145 | Rc<Context>, |
146 | ) -> Pin<Box<dyn Future<Output = Result<serde_json::Value>>>>, |
147 | >, |
148 | >, |
149 | ); |
150 | |
151 | impl RequestHandler { |
152 | pub fn register< |
153 | R: lsp_types::request::Request, |
154 | Fut: Future<Output = Result<R::Result>> + 'static, |
155 | >( |
156 | &mut self, |
157 | handler: fn(R::Params, Rc<Context>) -> Fut, |
158 | ) where |
159 | R::Params: 'static, |
160 | { |
161 | self.0.insert( |
162 | R::METHOD, |
163 | v:Box::new(move |value: Value, ctx: Rc| { |
164 | Box::pin(async move { |
165 | let params: ::Params = serde_json::from_value(value) |
166 | .map_err(|e: Error| format!("error when deserializing request: {e:?}" ))?; |
167 | handler(params, ctx).await.map(|x: ::Result| serde_json::to_value(x).unwrap()) |
168 | }) |
169 | }), |
170 | ); |
171 | } |
172 | } |
173 | |
174 | pub fn server_initialize_result(client_cap: &ClientCapabilities) -> InitializeResult { |
175 | InitializeResult { |
176 | capabilities: ServerCapabilities { |
177 | completion_provider: Some(CompletionOptions { |
178 | resolve_provider: None, |
179 | trigger_characters: Some(vec!["." .to_owned()]), |
180 | work_done_progress_options: WorkDoneProgressOptions::default(), |
181 | all_commit_characters: None, |
182 | completion_item: None, |
183 | }), |
184 | definition_provider: Some(OneOf::Left(true)), |
185 | text_document_sync: Some(TextDocumentSyncCapability::Kind( |
186 | lsp_types::TextDocumentSyncKind::FULL, |
187 | )), |
188 | code_action_provider: Some(CodeActionProviderCapability::Simple(true)), |
189 | execute_command_provider: Some(lsp_types::ExecuteCommandOptions { |
190 | commands: command_list(), |
191 | ..Default::default() |
192 | }), |
193 | document_symbol_provider: Some(OneOf::Left(true)), |
194 | color_provider: Some(true.into()), |
195 | code_lens_provider: Some(CodeLensOptions { resolve_provider: Some(true) }), |
196 | semantic_tokens_provider: Some( |
197 | SemanticTokensOptions { |
198 | legend: SemanticTokensLegend { |
199 | token_types: semantic_tokens::LEGEND_TYPES.to_vec(), |
200 | token_modifiers: semantic_tokens::LEGEND_MODS.to_vec(), |
201 | }, |
202 | full: Some(SemanticTokensFullOptions::Bool(true)), |
203 | ..Default::default() |
204 | } |
205 | .into(), |
206 | ), |
207 | document_highlight_provider: Some(OneOf::Left(true)), |
208 | rename_provider: Some( |
209 | if client_cap |
210 | .text_document |
211 | .as_ref() |
212 | .and_then(|td| td.rename.as_ref()) |
213 | .and_then(|r| r.prepare_support) |
214 | .unwrap_or(false) |
215 | { |
216 | OneOf::Right(RenameOptions { |
217 | prepare_provider: Some(true), |
218 | work_done_progress_options: WorkDoneProgressOptions::default(), |
219 | }) |
220 | } else { |
221 | OneOf::Left(true) |
222 | }, |
223 | ), |
224 | document_formatting_provider: Some(OneOf::Left(true)), |
225 | ..ServerCapabilities::default() |
226 | }, |
227 | server_info: Some(ServerInfo { |
228 | name: env!("CARGO_PKG_NAME" ).to_string(), |
229 | version: Some(env!("CARGO_PKG_VERSION" ).to_string()), |
230 | }), |
231 | offset_encoding: Some("utf-8" .to_string()), |
232 | } |
233 | } |
234 | |
235 | pub fn register_request_handlers(rh: &mut RequestHandler) { |
236 | rh.register::<GotoDefinition, _>(|params, ctx| async move { |
237 | let document_cache = &mut ctx.document_cache.borrow_mut(); |
238 | let result = token_descr( |
239 | document_cache, |
240 | ¶ms.text_document_position_params.text_document.uri, |
241 | ¶ms.text_document_position_params.position, |
242 | ) |
243 | .and_then(|token| goto::goto_definition(document_cache, token.0)); |
244 | Ok(result) |
245 | }); |
246 | rh.register::<Completion, _>(|params, ctx| async move { |
247 | let document_cache = &mut ctx.document_cache.borrow_mut(); |
248 | |
249 | let result = token_descr( |
250 | document_cache, |
251 | ¶ms.text_document_position.text_document.uri, |
252 | ¶ms.text_document_position.position, |
253 | ) |
254 | .and_then(|token| { |
255 | completion::completion_at( |
256 | document_cache, |
257 | token.0, |
258 | token.1, |
259 | ctx.init_param |
260 | .capabilities |
261 | .text_document |
262 | .as_ref() |
263 | .and_then(|t| t.completion.as_ref()), |
264 | ) |
265 | .map(Into::into) |
266 | }); |
267 | Ok(result) |
268 | }); |
269 | rh.register::<HoverRequest, _>(|_params, _ctx| async move { |
270 | /*let result = |
271 | token_descr(document_cache, params.text_document_position_params).map(|x| Hover { |
272 | contents: lsp_types::HoverContents::Scalar(MarkedString::from_language_code( |
273 | "text".into(), |
274 | format!("{:?}", x.token), |
275 | )), |
276 | range: None, |
277 | }); |
278 | let resp = Response::new_ok(id, result); |
279 | connection.sender.send(Message::Response(resp))?;*/ |
280 | Ok(None::<Hover>) |
281 | }); |
282 | rh.register::<CodeActionRequest, _>(|params, ctx| async move { |
283 | let document_cache = &mut ctx.document_cache.borrow_mut(); |
284 | |
285 | let result = token_descr(document_cache, ¶ms.text_document.uri, ¶ms.range.start) |
286 | .and_then(|(token, _)| { |
287 | get_code_actions(document_cache, token, &ctx.init_param.capabilities) |
288 | }); |
289 | Ok(result) |
290 | }); |
291 | rh.register::<ExecuteCommand, _>(|params, ctx| async move { |
292 | if params.command.as_str() == SHOW_PREVIEW_COMMAND { |
293 | #[cfg (any(feature = "preview-builtin" , feature = "preview-external" ))] |
294 | show_preview_command(¶ms.arguments, &ctx)?; |
295 | return Ok(None::<serde_json::Value>); |
296 | } |
297 | if params.command.as_str() == QUERY_PROPERTIES_COMMAND { |
298 | return Ok(Some(query_properties_command(¶ms.arguments, &ctx)?)); |
299 | } |
300 | if params.command.as_str() == SET_BINDING_COMMAND { |
301 | return Ok(Some(set_binding_command(¶ms.arguments, &ctx).await?)); |
302 | } |
303 | if params.command.as_str() == REMOVE_BINDING_COMMAND { |
304 | return Ok(Some(remove_binding_command(¶ms.arguments, &ctx).await?)); |
305 | } |
306 | Ok(None::<serde_json::Value>) |
307 | }); |
308 | rh.register::<DocumentColor, _>(|params, ctx| async move { |
309 | let document_cache = &mut ctx.document_cache.borrow_mut(); |
310 | Ok(get_document_color(document_cache, ¶ms.text_document).unwrap_or_default()) |
311 | }); |
312 | rh.register::<ColorPresentationRequest, _>(|params, _ctx| async move { |
313 | // Convert the color from the color picker to a string representation. This could try to produce a minimal |
314 | // representation. |
315 | let requested_color = params.color; |
316 | |
317 | let color_literal = if requested_color.alpha < 1. { |
318 | format!( |
319 | "# {:0>2x}{:0>2x}{:0>2x}{:0>2x}" , |
320 | (requested_color.red * 255.) as u8, |
321 | (requested_color.green * 255.) as u8, |
322 | (requested_color.blue * 255.) as u8, |
323 | (requested_color.alpha * 255.) as u8 |
324 | ) |
325 | } else { |
326 | format!( |
327 | "# {:0>2x}{:0>2x}{:0>2x}" , |
328 | (requested_color.red * 255.) as u8, |
329 | (requested_color.green * 255.) as u8, |
330 | (requested_color.blue * 255.) as u8, |
331 | ) |
332 | }; |
333 | |
334 | Ok(vec![ColorPresentation { label: color_literal, ..Default::default() }]) |
335 | }); |
336 | rh.register::<DocumentSymbolRequest, _>(|params, ctx| async move { |
337 | let document_cache = &mut ctx.document_cache.borrow_mut(); |
338 | Ok(get_document_symbols(document_cache, ¶ms.text_document)) |
339 | }); |
340 | rh.register::<CodeLensRequest, _>(|params, ctx| async move { |
341 | let document_cache = &mut ctx.document_cache.borrow_mut(); |
342 | Ok(get_code_lenses(document_cache, ¶ms.text_document)) |
343 | }); |
344 | rh.register::<SemanticTokensFullRequest, _>(|params, ctx| async move { |
345 | let document_cache = &mut ctx.document_cache.borrow_mut(); |
346 | Ok(semantic_tokens::get_semantic_tokens(document_cache, ¶ms.text_document)) |
347 | }); |
348 | rh.register::<DocumentHighlightRequest, _>(|_params, ctx| async move { |
349 | let document_cache = &mut ctx.document_cache.borrow_mut(); |
350 | let uri = _params.text_document_position_params.text_document.uri; |
351 | if let Some((tk, offset)) = |
352 | token_descr(document_cache, &uri, &_params.text_document_position_params.position) |
353 | { |
354 | let p = tk.parent(); |
355 | if p.kind() == SyntaxKind::QualifiedName |
356 | && p.parent().map_or(false, |n| n.kind() == SyntaxKind::Element) |
357 | { |
358 | if let Some(range) = util::map_node(&p) { |
359 | ctx.server_notifier.send_message_to_preview( |
360 | common::LspToPreviewMessage::HighlightFromEditor { url: Some(uri), offset }, |
361 | ); |
362 | return Ok(Some(vec![lsp_types::DocumentHighlight { range, kind: None }])); |
363 | } |
364 | } |
365 | |
366 | if let Some(value) = find_element_id_for_highlight(&tk, &p) { |
367 | ctx.server_notifier.send_message_to_preview( |
368 | common::LspToPreviewMessage::HighlightFromEditor { url: None, offset: 0 }, |
369 | ); |
370 | return Ok(Some( |
371 | value |
372 | .into_iter() |
373 | .map(|r| lsp_types::DocumentHighlight { |
374 | range: util::map_range(&p.source_file, r), |
375 | kind: None, |
376 | }) |
377 | .collect(), |
378 | )); |
379 | } |
380 | } |
381 | ctx.server_notifier.send_message_to_preview( |
382 | common::LspToPreviewMessage::HighlightFromEditor { url: None, offset: 0 }, |
383 | ); |
384 | Ok(None) |
385 | }); |
386 | rh.register::<Rename, _>(|params, ctx| async move { |
387 | let mut document_cache = ctx.document_cache.borrow_mut(); |
388 | let uri = params.text_document_position.text_document.uri; |
389 | if let Some((tk, _off)) = |
390 | token_descr(&mut document_cache, &uri, ¶ms.text_document_position.position) |
391 | { |
392 | let p = tk.parent(); |
393 | let version = p.source_file.version(); |
394 | if let Some(value) = find_element_id_for_highlight(&tk, &tk.parent()) { |
395 | let edits: Vec<_> = value |
396 | .into_iter() |
397 | .map(|r| TextEdit { |
398 | range: util::map_range(&p.source_file, r), |
399 | new_text: params.new_name.clone(), |
400 | }) |
401 | .collect(); |
402 | return Ok(Some(common::create_workspace_edit(uri, version, edits))); |
403 | } |
404 | }; |
405 | Err("This symbol cannot be renamed. (Only element id can be renamed at the moment)" .into()) |
406 | }); |
407 | rh.register::<PrepareRenameRequest, _>(|params, ctx| async move { |
408 | let mut document_cache = ctx.document_cache.borrow_mut(); |
409 | let uri = params.text_document.uri; |
410 | if let Some((tk, _off)) = token_descr(&mut document_cache, &uri, ¶ms.position) { |
411 | if find_element_id_for_highlight(&tk, &tk.parent()).is_some() { |
412 | return Ok(util::map_token(&tk).map(PrepareRenameResponse::Range)); |
413 | } |
414 | }; |
415 | Ok(None) |
416 | }); |
417 | rh.register::<Formatting, _>(|params, ctx| async move { |
418 | let document_cache = ctx.document_cache.borrow_mut(); |
419 | Ok(formatting::format_document(params, &document_cache)) |
420 | }); |
421 | } |
422 | |
423 | #[cfg (any(feature = "preview-builtin" , feature = "preview-external" ))] |
424 | pub fn show_preview_command(params: &[serde_json::Value], ctx: &Rc<Context>) -> Result<()> { |
425 | let document_cache = &mut ctx.document_cache.borrow_mut(); |
426 | let config = &document_cache.documents.compiler_config; |
427 | |
428 | let e = || "InvalidParameter" ; |
429 | |
430 | let url: Url = serde_json::from_value(params.first().ok_or_else(e)?.clone())?; |
431 | // Normalize the URL to make sure it is encoded the same way as what the preview expect from other URLs |
432 | let url = Url::from_file_path(uri_to_file(&url).ok_or_else(e)?).map_err(|_| e())?; |
433 | |
434 | let component = |
435 | params.get(1).and_then(|v| v.as_str()).filter(|v| !v.is_empty()).map(|v| v.to_string()); |
436 | |
437 | let c = common::PreviewComponent { |
438 | url, |
439 | component, |
440 | style: config.style.clone().unwrap_or_default(), |
441 | }; |
442 | ctx.to_show.replace(Some(c.clone())); |
443 | ctx.server_notifier.send_message_to_preview(common::LspToPreviewMessage::ShowPreview(c)); |
444 | |
445 | // Update known Components |
446 | report_known_components(document_cache, ctx); |
447 | |
448 | Ok(()) |
449 | } |
450 | |
451 | pub fn query_properties_command( |
452 | params: &[serde_json::Value], |
453 | ctx: &Rc<Context>, |
454 | ) -> Result<serde_json::Value> { |
455 | let document_cache = &mut ctx.document_cache.borrow_mut(); |
456 | |
457 | let text_document_uri = serde_json::from_value::<lsp_types::TextDocumentIdentifier>( |
458 | params.first().ok_or("No text document provided" )?.clone(), |
459 | )? |
460 | .uri; |
461 | let position = serde_json::from_value::<lsp_types::Position>( |
462 | params.get(1).ok_or("No position provided" )?.clone(), |
463 | )?; |
464 | |
465 | let source_version = if let Some(v) = document_cache.document_version(&text_document_uri) { |
466 | Some(v) |
467 | } else { |
468 | return Ok(serde_json::to_value(properties::QueryPropertyResponse::no_element_response( |
469 | text_document_uri.to_string(), |
470 | -1, |
471 | )) |
472 | .expect("Failed to serialize none-element property query result!" )); |
473 | }; |
474 | |
475 | if let Some(element) = |
476 | element_at_position(&document_cache.documents, &text_document_uri, &position) |
477 | { |
478 | properties::query_properties(&text_document_uri, source_version, &element) |
479 | .map(|r| serde_json::to_value(r).expect("Failed to serialize property query result!" )) |
480 | } else { |
481 | Ok(serde_json::to_value(properties::QueryPropertyResponse::no_element_response( |
482 | text_document_uri.to_string(), |
483 | source_version.unwrap_or(i32::MIN), |
484 | )) |
485 | .expect("Failed to serialize none-element property query result!" )) |
486 | } |
487 | } |
488 | |
489 | pub async fn set_binding_command( |
490 | params: &[serde_json::Value], |
491 | ctx: &Rc<Context>, |
492 | ) -> Result<serde_json::Value> { |
493 | let text_document = serde_json::from_value::<lsp_types::OptionalVersionedTextDocumentIdentifier>( |
494 | params.first().ok_or("No text document provided" )?.clone(), |
495 | )?; |
496 | let element_range = serde_json::from_value::<lsp_types::Range>( |
497 | params.get(1).ok_or("No element range provided" )?.clone(), |
498 | )?; |
499 | let property_name = serde_json::from_value::<String>( |
500 | params.get(2).ok_or("No property name provided" )?.clone(), |
501 | )?; |
502 | let new_expression = |
503 | serde_json::from_value::<String>(params.get(3).ok_or("No expression provided" )?.clone())?; |
504 | let dry_run = { |
505 | if let Some(p) = params.get(4) { |
506 | serde_json::from_value::<bool>(p.clone()) |
507 | } else { |
508 | Ok(true) |
509 | } |
510 | }?; |
511 | |
512 | let (result, edit) = { |
513 | let document_cache = &mut ctx.document_cache.borrow_mut(); |
514 | let uri = text_document.uri; |
515 | let version = document_cache.document_version(&uri); |
516 | if let Some(source_version) = text_document.version { |
517 | if let Some(current_version) = version { |
518 | if current_version != source_version { |
519 | return Err( |
520 | "Document version mismatch. Please refresh your property information" |
521 | .into(), |
522 | ); |
523 | } |
524 | } else { |
525 | return Err(format!("Document with uri {uri} not found in cache" ).into()); |
526 | } |
527 | } |
528 | |
529 | let element = element_at_position(&document_cache.documents, &uri, &element_range.start) |
530 | .ok_or_else(|| { |
531 | format!("No element found at the given start position {:?}" , &element_range.start) |
532 | })?; |
533 | |
534 | let node_range = |
535 | element.with_element_node(|node| util::map_node(node)).ok_or("Failed to map node" )?; |
536 | |
537 | if node_range.start != element_range.start { |
538 | return Err(format!( |
539 | "Element found, but does not start at the expected place () {:?} != {:?})." , |
540 | node_range.start, element_range.start |
541 | ) |
542 | .into()); |
543 | } |
544 | if node_range.end != element_range.end { |
545 | return Err(format!( |
546 | "Element found, but does not end at the expected place () {:?} != {:?})." , |
547 | node_range.end, element_range.end |
548 | ) |
549 | .into()); |
550 | } |
551 | |
552 | properties::set_binding( |
553 | document_cache, |
554 | &uri, |
555 | version, |
556 | &element, |
557 | &property_name, |
558 | new_expression, |
559 | )? |
560 | }; |
561 | |
562 | if !dry_run { |
563 | if let Some(edit) = edit { |
564 | let response = ctx |
565 | .server_notifier |
566 | .send_request::<lsp_types::request::ApplyWorkspaceEdit>( |
567 | lsp_types::ApplyWorkspaceEditParams { label: Some("set binding" .into()), edit }, |
568 | )? |
569 | .await?; |
570 | if !response.applied { |
571 | return Err(response |
572 | .failure_reason |
573 | .unwrap_or("Operation failed, no specific reason given" .into()) |
574 | .into()); |
575 | } |
576 | } |
577 | } |
578 | |
579 | Ok(serde_json::to_value(result).expect("Failed to serialize set_binding result!" )) |
580 | } |
581 | |
582 | pub async fn remove_binding_command( |
583 | params: &[serde_json::Value], |
584 | ctx: &Rc<Context>, |
585 | ) -> Result<serde_json::Value> { |
586 | let text_document = serde_json::from_value::<lsp_types::OptionalVersionedTextDocumentIdentifier>( |
587 | params.first().ok_or("No text document provided" )?.clone(), |
588 | )?; |
589 | let element_range = serde_json::from_value::<lsp_types::Range>( |
590 | params.get(1).ok_or("No element range provided" )?.clone(), |
591 | )?; |
592 | let property_name = serde_json::from_value::<String>( |
593 | params.get(2).ok_or("No property name provided" )?.clone(), |
594 | )?; |
595 | |
596 | let edit = { |
597 | let document_cache = &mut ctx.document_cache.borrow_mut(); |
598 | let uri = text_document.uri; |
599 | let version = document_cache.document_version(&uri); |
600 | |
601 | if let Some(source_version) = text_document.version { |
602 | if let Some(current_version) = version { |
603 | if current_version != source_version { |
604 | return Err( |
605 | "Document version mismatch. Please refresh your property information" |
606 | .into(), |
607 | ); |
608 | } |
609 | } else { |
610 | return Err(format!("Document with uri {uri} not found in cache" ).into()); |
611 | } |
612 | } |
613 | |
614 | let element = element_at_position(&document_cache.documents, &uri, &element_range.start) |
615 | .ok_or_else(|| { |
616 | format!("No element found at the given start position {:?}" , &element_range.start) |
617 | })?; |
618 | |
619 | let node_range = |
620 | element.with_element_node(|node| util::map_node(node)).ok_or("Failed to map node" )?; |
621 | |
622 | if node_range.start != element_range.start { |
623 | return Err(format!( |
624 | "Element found, but does not start at the expected place () {:?} != {:?})." , |
625 | node_range.start, element_range.start |
626 | ) |
627 | .into()); |
628 | } |
629 | if node_range.end != element_range.end { |
630 | return Err(format!( |
631 | "Element found, but does not end at the expected place () {:?} != {:?})." , |
632 | node_range.end, element_range.end |
633 | ) |
634 | .into()); |
635 | } |
636 | |
637 | properties::remove_binding(uri, version, &element, &property_name)? |
638 | }; |
639 | |
640 | let response = ctx |
641 | .server_notifier |
642 | .send_request::<lsp_types::request::ApplyWorkspaceEdit>( |
643 | lsp_types::ApplyWorkspaceEditParams { label: Some("set binding" .into()), edit }, |
644 | )? |
645 | .await?; |
646 | |
647 | if !response.applied { |
648 | return Err(response |
649 | .failure_reason |
650 | .unwrap_or("Operation failed, no specific reason given" .into()) |
651 | .into()); |
652 | } |
653 | |
654 | Ok(serde_json::to_value(()).expect("Failed to serialize ()!" )) |
655 | } |
656 | |
657 | pub(crate) async fn reload_document_impl( |
658 | ctx: Option<&Rc<Context>>, |
659 | mut content: String, |
660 | url: lsp_types::Url, |
661 | version: Option<i32>, |
662 | document_cache: &mut DocumentCache, |
663 | ) -> HashMap<Url, Vec<lsp_types::Diagnostic>> { |
664 | let Some(path) = uri_to_file(&url) else { return Default::default() }; |
665 | // Normalize the URL |
666 | let Ok(url) = Url::from_file_path(path.clone()) else { return Default::default() }; |
667 | if path.extension().map_or(false, |e| e == "rs" ) { |
668 | content = match i_slint_compiler::lexer::extract_rust_macro(content) { |
669 | Some(content) => content, |
670 | // A rust file without a rust macro, just ignore it |
671 | None => return [(url, vec![])].into_iter().collect(), |
672 | }; |
673 | } |
674 | |
675 | if let Some(ctx) = ctx { |
676 | ctx.server_notifier.send_message_to_preview(common::LspToPreviewMessage::SetContents { |
677 | url: common::VersionedUrl::new(url, version), |
678 | contents: content.clone(), |
679 | }); |
680 | } |
681 | let mut diag = BuildDiagnostics::default(); |
682 | document_cache.documents.load_file(&path, version, &path, content, false, &mut diag).await; |
683 | |
684 | // Always provide diagnostics for all files. Empty diagnostics clear any previous ones. |
685 | let mut lsp_diags: HashMap<Url, Vec<lsp_types::Diagnostic>> = core::iter::once(&path) |
686 | .chain(diag.all_loaded_files.iter()) |
687 | .map(|path| { |
688 | let uri = Url::from_file_path(path).unwrap(); |
689 | (uri, Default::default()) |
690 | }) |
691 | .collect(); |
692 | |
693 | for d in diag.into_iter() { |
694 | #[cfg (not(target_arch = "wasm32" ))] |
695 | if d.source_file().unwrap().is_relative() { |
696 | continue; |
697 | } |
698 | let uri = Url::from_file_path(d.source_file().unwrap()).unwrap(); |
699 | lsp_diags.entry(uri).or_default().push(util::to_lsp_diag(&d)); |
700 | } |
701 | |
702 | lsp_diags |
703 | } |
704 | |
705 | fn report_known_components(document_cache: &mut DocumentCache, ctx: &Rc<Context>) { |
706 | let mut components: Vec = Vec::new(); |
707 | component_catalog::builtin_components(document_cache, &mut components); |
708 | component_catalog::all_exported_components( |
709 | document_cache, |
710 | &mut |ci| ci.is_global, |
711 | &mut components, |
712 | ); |
713 | |
714 | components.sort_by(|a: &ComponentInformation, b: &ComponentInformation| a.name.cmp(&b.name)); |
715 | |
716 | let url: Option = ctx.to_show.borrow().as_ref().map(|pc: &PreviewComponent| { |
717 | let url: Url = pc.url.clone(); |
718 | let file: PathBuf = PathBuf::from(url.to_string()); |
719 | let version: Option = document_cache.document_version(&url); |
720 | |
721 | component_catalog::file_local_components(document_cache, &file, &mut components); |
722 | |
723 | common::VersionedUrl::new(url, version) |
724 | }); |
725 | |
726 | ctx.server_notifier |
727 | .send_message_to_preview(message:common::LspToPreviewMessage::KnownComponents { url, components }); |
728 | } |
729 | |
730 | pub async fn reload_document( |
731 | ctx: &Rc<Context>, |
732 | content: String, |
733 | url: lsp_types::Url, |
734 | version: Option<i32>, |
735 | document_cache: &mut DocumentCache, |
736 | ) -> Result<()> { |
737 | let lsp_diags: HashMap> = |
738 | reload_document_impl(ctx:Some(ctx), content, url.clone(), version, document_cache).await; |
739 | |
740 | for (uri: Url, diagnostics: Vec) in lsp_diags { |
741 | ctx.server_notifier.send_notification( |
742 | method:"textDocument/publishDiagnostics" .into(), |
743 | params:PublishDiagnosticsParams { uri, diagnostics, version: None }, |
744 | )?; |
745 | } |
746 | |
747 | // Tell Preview about the Components: |
748 | report_known_components(document_cache, ctx); |
749 | |
750 | Ok(()) |
751 | } |
752 | |
753 | fn get_document_and_offset<'a>( |
754 | type_loader: &'a TypeLoader, |
755 | text_document_uri: &'a Url, |
756 | pos: &'a Position, |
757 | ) -> Option<(&'a i_slint_compiler::object_tree::Document, u32)> { |
758 | let path: PathBuf = uri_to_file(text_document_uri)?; |
759 | let doc: &Document = type_loader.get_document(&path)?; |
760 | let o: u32 = doc.node.as_ref()?.source_file.offset(pos.line as usize + 1, pos.character as usize + 1) |
761 | as u32; |
762 | doc.node.as_ref()?.text_range().contains_inclusive(o.into()).then_some((doc, o)) |
763 | } |
764 | |
765 | fn element_contains( |
766 | element: &i_slint_compiler::object_tree::ElementRc, |
767 | offset: u32, |
768 | ) -> Option<usize> { |
769 | elementIter<'_, (Element, Option<…>)> |
770 | .borrow() |
771 | .debug |
772 | .iter() |
773 | .position(|n: &(Element, Option)| n.0.parent().map_or(false, |n| n.text_range().contains(offset.into()))) |
774 | } |
775 | |
776 | fn element_node_contains(element: &common::ElementRcNode, offset: u32) -> bool { |
777 | element.with_element_node(|node: &Element| { |
778 | node.parent().map_or(false, |n| n.text_range().contains(offset.into())) |
779 | }) |
780 | } |
781 | |
782 | pub fn element_at_position( |
783 | type_loader: &TypeLoader, |
784 | text_document_uri: &Url, |
785 | pos: &Position, |
786 | ) -> Option<common::ElementRcNode> { |
787 | let (doc, offset) = get_document_and_offset(type_loader, text_document_uri, pos)?; |
788 | |
789 | for component in &doc.inner_components { |
790 | let root_element = component.root_element.clone(); |
791 | let Some(root_debug_index) = element_contains(&root_element, offset) else { |
792 | continue; |
793 | }; |
794 | |
795 | let mut element = |
796 | common::ElementRcNode { element: root_element, debug_index: root_debug_index }; |
797 | while element_node_contains(&element, offset) { |
798 | if let Some((c, i)) = element |
799 | .element |
800 | .clone() |
801 | .borrow() |
802 | .children |
803 | .iter() |
804 | .find_map(|c| element_contains(c, offset).map(|i| (c, i))) |
805 | { |
806 | element = common::ElementRcNode { element: c.clone(), debug_index: i }; |
807 | } else { |
808 | return Some(element); |
809 | } |
810 | } |
811 | } |
812 | None |
813 | } |
814 | |
815 | /// return the token, and the offset within the file |
816 | fn token_descr( |
817 | document_cache: &mut DocumentCache, |
818 | text_document_uri: &Url, |
819 | pos: &Position, |
820 | ) -> Option<(SyntaxToken, u32)> { |
821 | let (doc: &Document, o: u32) = get_document_and_offset(&document_cache.documents, text_document_uri, pos)?; |
822 | let node: &Document = doc.node.as_ref()?; |
823 | |
824 | let token: SyntaxToken = token_at_offset(doc:node, offset:o)?; |
825 | Some((token, o)) |
826 | } |
827 | |
828 | /// Return the token that matches best the token at cursor position |
829 | pub fn token_at_offset(doc: &syntax_nodes::Document, offset: u32) -> Option<SyntaxToken> { |
830 | let mut taf = doc.token_at_offset(offset.into()); |
831 | let token: SyntaxToken = match (taf.next(), taf.next()) { |
832 | (None, _) => doc.last_token()?, |
833 | (Some(t), None) => t, |
834 | (Some(l), Some(r)) => match (l.kind(), r.kind()) { |
835 | // Prioritize identifier |
836 | (SyntaxKind::Identifier, _) => l, |
837 | (_, SyntaxKind::Identifier) => r, |
838 | // then the dot |
839 | (SyntaxKind::Dot, _) => l, |
840 | (_, SyntaxKind::Dot) => r, |
841 | // de-prioritize the white spaces |
842 | (SyntaxKind::Whitespace, _) => r, |
843 | (SyntaxKind::Comment, _) => r, |
844 | (_, SyntaxKind::Whitespace) => l, |
845 | (_, SyntaxKind::Comment) => l, |
846 | _ => l, |
847 | }, |
848 | }; |
849 | Some(SyntaxToken { token, source_file: doc.source_file.clone() }) |
850 | } |
851 | |
852 | fn has_experimental_client_capability(capabilities: &ClientCapabilities, name: &str) -> bool { |
853 | capabilities |
854 | .experimental |
855 | .as_ref() |
856 | .and_then(|o| o.get(name).and_then(|v| v.as_bool())) |
857 | .unwrap_or(default:false) |
858 | } |
859 | |
860 | fn get_code_actions( |
861 | document_cache: &mut DocumentCache, |
862 | token: SyntaxToken, |
863 | client_capabilities: &ClientCapabilities, |
864 | ) -> Option<Vec<CodeActionOrCommand>> { |
865 | let node = token.parent(); |
866 | let uri = Url::from_file_path(token.source_file.path()).ok()?; |
867 | let mut result = vec![]; |
868 | |
869 | let component = syntax_nodes::Component::new(node.clone()) |
870 | .or_else(|| { |
871 | syntax_nodes::DeclaredIdentifier::new(node.clone()) |
872 | .and_then(|n| n.parent()) |
873 | .and_then(syntax_nodes::Component::new) |
874 | }) |
875 | .or_else(|| { |
876 | syntax_nodes::QualifiedName::new(node.clone()) |
877 | .and_then(|n| n.parent()) |
878 | .and_then(syntax_nodes::Element::new) |
879 | .and_then(|n| n.parent()) |
880 | .and_then(syntax_nodes::Component::new) |
881 | }); |
882 | |
883 | #[cfg (any(feature = "preview-builtin" , feature = "preview-external" ))] |
884 | { |
885 | if let Some(component) = &component { |
886 | if let Some(component_name) = |
887 | i_slint_compiler::parser::identifier_text(&component.DeclaredIdentifier()) |
888 | { |
889 | result.push(CodeActionOrCommand::Command(create_show_preview_command( |
890 | false, |
891 | &uri, |
892 | &component_name, |
893 | ))) |
894 | } |
895 | } |
896 | } |
897 | |
898 | if token.kind() == SyntaxKind::StringLiteral && node.kind() == SyntaxKind::Expression { |
899 | let r = util::map_range(&token.source_file, node.text_range()); |
900 | let edits = vec![ |
901 | TextEdit::new(lsp_types::Range::new(r.start, r.start), "@tr(" .into()), |
902 | TextEdit::new(lsp_types::Range::new(r.end, r.end), ")" .into()), |
903 | ]; |
904 | result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { |
905 | title: "Wrap in `@tr()`" .into(), |
906 | edit: common::create_workspace_edit_from_source_file(&token.source_file, edits), |
907 | ..Default::default() |
908 | })); |
909 | } else if token.kind() == SyntaxKind::Identifier |
910 | && node.kind() == SyntaxKind::QualifiedName |
911 | && node.parent().map(|n| n.kind()) == Some(SyntaxKind::Element) |
912 | { |
913 | let is_lookup_error = { |
914 | let global_tr = document_cache.documents.global_type_registry.borrow(); |
915 | let tr = document_cache |
916 | .documents |
917 | .get_document(token.source_file.path()) |
918 | .map(|doc| &doc.local_registry) |
919 | .unwrap_or(&global_tr); |
920 | util::lookup_current_element_type(node.clone(), tr).is_none() |
921 | }; |
922 | if is_lookup_error { |
923 | // Couldn't lookup the element, there is probably an error. Suggest an edit |
924 | let text = token.text(); |
925 | completion::build_import_statements_edits( |
926 | &token, |
927 | document_cache, |
928 | &mut |name| name == text, |
929 | &mut |_name, file, edit| { |
930 | result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { |
931 | title: format!("Add import from \"{file}\"" ), |
932 | kind: Some(lsp_types::CodeActionKind::QUICKFIX), |
933 | edit: common::create_workspace_edit_from_source_file( |
934 | &token.source_file, |
935 | vec![edit], |
936 | ), |
937 | ..Default::default() |
938 | })) |
939 | }, |
940 | ); |
941 | } |
942 | |
943 | if has_experimental_client_capability(client_capabilities, "snippetTextEdit" ) { |
944 | let r = util::map_range(&token.source_file, node.parent().unwrap().text_range()); |
945 | let element = element_at_position(&document_cache.documents, &uri, &r.start); |
946 | let element_indent = element.as_ref().and_then(util::find_element_indent); |
947 | let indented_lines = node |
948 | .parent() |
949 | .unwrap() |
950 | .text() |
951 | .to_string() |
952 | .lines() |
953 | .map( |
954 | |line| if line.is_empty() { line.to_string() } else { format!(" {}" , line) }, |
955 | ) |
956 | .collect::<Vec<String>>(); |
957 | let edits = vec![TextEdit::new( |
958 | lsp_types::Range::new(r.start, r.end), |
959 | format!( |
960 | "$ {{0:element }} {{\n{}{}\n}}" , |
961 | element_indent.unwrap_or("" .into()), |
962 | indented_lines.join(" \n" ) |
963 | ), |
964 | )]; |
965 | result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { |
966 | title: "Wrap in element" .into(), |
967 | kind: Some(lsp_types::CodeActionKind::REFACTOR), |
968 | edit: common::create_workspace_edit_from_source_file(&token.source_file, edits), |
969 | ..Default::default() |
970 | })); |
971 | |
972 | // Collect all normal, repeated, and conditional sub-elements and any |
973 | // whitespace in between for substituting the parent element with its |
974 | // sub-elements, dropping its own properties, callbacks etc. |
975 | fn is_sub_element(kind: SyntaxKind) -> bool { |
976 | matches!( |
977 | kind, |
978 | SyntaxKind::SubElement |
979 | | SyntaxKind::RepeatedElement |
980 | | SyntaxKind::ConditionalElement |
981 | ) |
982 | } |
983 | let sub_elements = node |
984 | .parent() |
985 | .unwrap() |
986 | .children_with_tokens() |
987 | .skip_while(|n| !is_sub_element(n.kind())) |
988 | .filter(|n| match n { |
989 | NodeOrToken::Node(_) => is_sub_element(n.kind()), |
990 | NodeOrToken::Token(t) => { |
991 | t.kind() == SyntaxKind::Whitespace |
992 | && t.next_sibling_or_token().map_or(false, |n| is_sub_element(n.kind())) |
993 | } |
994 | }) |
995 | .collect::<Vec<_>>(); |
996 | |
997 | if match component { |
998 | // A top-level component element can only be removed if it contains |
999 | // exactly one sub-element (without any condition or assignment) |
1000 | // that can substitute the component element. |
1001 | Some(_) => { |
1002 | sub_elements.len() == 1 |
1003 | && sub_elements.first().and_then(|n| { |
1004 | n.as_node().unwrap().first_child_or_token().map(|n| n.kind()) |
1005 | }) == Some(SyntaxKind::Element) |
1006 | } |
1007 | // Any other element can be removed in favor of one or more sub-elements. |
1008 | None => sub_elements.iter().any(|n| n.kind() == SyntaxKind::SubElement), |
1009 | } { |
1010 | let unindented_lines = sub_elements |
1011 | .iter() |
1012 | .map(|n| match n { |
1013 | NodeOrToken::Node(n) => n |
1014 | .text() |
1015 | .to_string() |
1016 | .lines() |
1017 | .map(|line| line.strip_prefix(" " ).unwrap_or(line).to_string()) |
1018 | .collect::<Vec<_>>() |
1019 | .join(" \n" ), |
1020 | NodeOrToken::Token(t) => { |
1021 | t.text().strip_suffix(" " ).unwrap_or(t.text()).to_string() |
1022 | } |
1023 | }) |
1024 | .collect::<Vec<String>>(); |
1025 | let edits = vec![TextEdit::new( |
1026 | lsp_types::Range::new(r.start, r.end), |
1027 | unindented_lines.concat(), |
1028 | )]; |
1029 | result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { |
1030 | title: "Remove element" .into(), |
1031 | kind: Some(lsp_types::CodeActionKind::REFACTOR), |
1032 | edit: common::create_workspace_edit_from_source_file(&token.source_file, edits), |
1033 | ..Default::default() |
1034 | })); |
1035 | } |
1036 | |
1037 | // We have already checked that the node is a qualified name of an element. |
1038 | // Check whether the element is a direct sub-element of another element |
1039 | // meaning that it can be repeated or made conditional. |
1040 | if node // QualifiedName |
1041 | .parent() // Element |
1042 | .unwrap() |
1043 | .parent() |
1044 | .filter(|n| n.kind() == SyntaxKind::SubElement) |
1045 | .and_then(|p| p.parent()) |
1046 | .is_some_and(|n| n.kind() == SyntaxKind::Element) |
1047 | { |
1048 | let edits = vec![TextEdit::new( |
1049 | lsp_types::Range::new(r.start, r.start), |
1050 | "for ${1:name}[index] in ${0:model} : " .to_string(), |
1051 | )]; |
1052 | result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { |
1053 | title: "Repeat element" .into(), |
1054 | kind: Some(lsp_types::CodeActionKind::REFACTOR), |
1055 | edit: common::create_workspace_edit_from_source_file(&token.source_file, edits), |
1056 | ..Default::default() |
1057 | })); |
1058 | |
1059 | let edits = vec![TextEdit::new( |
1060 | lsp_types::Range::new(r.start, r.start), |
1061 | "if ${0:condition} : " .to_string(), |
1062 | )]; |
1063 | result.push(CodeActionOrCommand::CodeAction(lsp_types::CodeAction { |
1064 | title: "Make conditional" .into(), |
1065 | kind: Some(lsp_types::CodeActionKind::REFACTOR), |
1066 | edit: common::create_workspace_edit_from_source_file(&token.source_file, edits), |
1067 | ..Default::default() |
1068 | })); |
1069 | } |
1070 | } |
1071 | } |
1072 | |
1073 | (!result.is_empty()).then_some(result) |
1074 | } |
1075 | |
1076 | fn get_document_color( |
1077 | document_cache: &mut DocumentCache, |
1078 | text_document: &lsp_types::TextDocumentIdentifier, |
1079 | ) -> Option<Vec<ColorInformation>> { |
1080 | let mut result = Vec::new(); |
1081 | let uri_path = uri_to_file(&text_document.uri)?; |
1082 | let doc = document_cache.documents.get_document(&uri_path)?; |
1083 | let root_node = doc.node.as_ref()?; |
1084 | let mut token = root_node.first_token()?; |
1085 | loop { |
1086 | if token.kind() == SyntaxKind::ColorLiteral { |
1087 | (|| -> Option<()> { |
1088 | let range = util::map_token(&token)?; |
1089 | let col = i_slint_compiler::literals::parse_color_literal(token.text())?; |
1090 | let shift = |s: u32| -> f32 { ((col >> s) & 0xff) as f32 / 255. }; |
1091 | result.push(ColorInformation { |
1092 | range, |
1093 | color: Color { |
1094 | alpha: shift(24), |
1095 | red: shift(16), |
1096 | green: shift(8), |
1097 | blue: shift(0), |
1098 | }, |
1099 | }); |
1100 | Some(()) |
1101 | })(); |
1102 | } |
1103 | token = match token.next_token() { |
1104 | Some(token) => token, |
1105 | None => break Some(result), |
1106 | } |
1107 | } |
1108 | } |
1109 | |
1110 | /// Retrieve the document outline |
1111 | fn get_document_symbols( |
1112 | document_cache: &mut DocumentCache, |
1113 | text_document: &lsp_types::TextDocumentIdentifier, |
1114 | ) -> Option<DocumentSymbolResponse> { |
1115 | let uri_path = uri_to_file(&text_document.uri)?; |
1116 | let doc = document_cache.documents.get_document(&uri_path)?; |
1117 | |
1118 | // DocumentSymbol doesn't implement default and some field depends on features or are deprecated |
1119 | let ds: DocumentSymbol = serde_json::from_value( |
1120 | serde_json::json!({ "name" : "" , "kind" : 255, "range" : lsp_types::Range::default(), "selectionRange" : lsp_types::Range::default() }) |
1121 | ) |
1122 | .unwrap(); |
1123 | |
1124 | let inner_components = doc.inner_components.clone(); |
1125 | let inner_types = doc.inner_types.clone(); |
1126 | |
1127 | let mut r = inner_components |
1128 | .iter() |
1129 | .filter_map(|c| { |
1130 | let root_element = c.root_element.borrow(); |
1131 | let element_node = &root_element.debug.first()?.0; |
1132 | let component_node = syntax_nodes::Component::new(element_node.parent()?)?; |
1133 | let selection_range = util::map_node(&component_node.DeclaredIdentifier())?; |
1134 | if c.id.is_empty() { |
1135 | // Symbols with empty names are invalid |
1136 | return None; |
1137 | } |
1138 | |
1139 | Some(DocumentSymbol { |
1140 | range: util::map_node(&component_node)?, |
1141 | selection_range, |
1142 | name: c.id.clone(), |
1143 | kind: if c.is_global() { |
1144 | lsp_types::SymbolKind::OBJECT |
1145 | } else { |
1146 | lsp_types::SymbolKind::CLASS |
1147 | }, |
1148 | children: gen_children(&c.root_element, &ds), |
1149 | ..ds.clone() |
1150 | }) |
1151 | }) |
1152 | .collect::<Vec<_>>(); |
1153 | |
1154 | r.extend(inner_types.iter().filter_map(|c| match c { |
1155 | Type::Struct { name: Some(name), node: Some(node), .. } => Some(DocumentSymbol { |
1156 | range: util::map_node(node.parent().as_ref()?)?, |
1157 | selection_range: util::map_node( |
1158 | &node.parent()?.child_node(SyntaxKind::DeclaredIdentifier)?, |
1159 | )?, |
1160 | name: name.clone(), |
1161 | kind: lsp_types::SymbolKind::STRUCT, |
1162 | ..ds.clone() |
1163 | }), |
1164 | Type::Enumeration(enumeration) => enumeration.node.as_ref().and_then(|node| { |
1165 | Some(DocumentSymbol { |
1166 | range: util::map_node(node)?, |
1167 | selection_range: util::map_node(&node.DeclaredIdentifier())?, |
1168 | name: enumeration.name.clone(), |
1169 | kind: lsp_types::SymbolKind::ENUM, |
1170 | ..ds.clone() |
1171 | }) |
1172 | }), |
1173 | _ => None, |
1174 | })); |
1175 | |
1176 | fn gen_children(elem: &ElementRc, ds: &DocumentSymbol) -> Option<Vec<DocumentSymbol>> { |
1177 | let r = elem |
1178 | .borrow() |
1179 | .children |
1180 | .iter() |
1181 | .filter_map(|child| { |
1182 | let e = child.borrow(); |
1183 | let element_node = &e.debug.first()?.0; |
1184 | let sub_element_node = element_node.parent()?; |
1185 | debug_assert_eq!(sub_element_node.kind(), SyntaxKind::SubElement); |
1186 | Some(DocumentSymbol { |
1187 | range: util::map_node(&sub_element_node)?, |
1188 | selection_range: util::map_node(element_node.QualifiedName().as_ref()?)?, |
1189 | name: e.base_type.to_string(), |
1190 | detail: (!e.id.is_empty()).then(|| e.id.clone()), |
1191 | kind: lsp_types::SymbolKind::VARIABLE, |
1192 | children: gen_children(child, ds), |
1193 | ..ds.clone() |
1194 | }) |
1195 | }) |
1196 | .collect::<Vec<_>>(); |
1197 | (!r.is_empty()).then_some(r) |
1198 | } |
1199 | |
1200 | r.sort_by(|a, b| a.range.start.cmp(&b.range.start)); |
1201 | |
1202 | Some(r.into()) |
1203 | } |
1204 | |
1205 | fn get_code_lenses( |
1206 | document_cache: &mut DocumentCache, |
1207 | text_document: &lsp_types::TextDocumentIdentifier, |
1208 | ) -> Option<Vec<CodeLens>> { |
1209 | if cfg!(any(feature = "preview-builtin" , feature = "preview-external" )) { |
1210 | let filepath: PathBuf = uri_to_file(&text_document.uri)?; |
1211 | let doc: &Document = document_cache.documents.get_document(&filepath)?; |
1212 | |
1213 | let inner_components: Vec> = doc.inner_components.clone(); |
1214 | |
1215 | let mut r: Vec = vec![]; |
1216 | |
1217 | // Handle preview lens |
1218 | r.extend(iter:inner_components.iter().filter(|c: &&Rc| !c.is_global()).filter_map(|c: &Rc| { |
1219 | Some(CodeLens { |
1220 | range: util::map_node(&c.root_element.borrow().debug.first()?.0)?, |
1221 | command: Some(create_show_preview_command(pretty:true, &text_document.uri, component_name:c.id.as_str())), |
1222 | data: None, |
1223 | }) |
1224 | })); |
1225 | |
1226 | Some(r) |
1227 | } else { |
1228 | None |
1229 | } |
1230 | } |
1231 | |
1232 | /// If the token is matching a Element ID, return the list of all element id in the same component |
1233 | fn find_element_id_for_highlight( |
1234 | token: &SyntaxToken, |
1235 | parent: &SyntaxNode, |
1236 | ) -> Option<Vec<rowan::TextRange>> { |
1237 | fn is_element_id(tk: &SyntaxToken, parent: &SyntaxNode) -> bool { |
1238 | if tk.kind() != SyntaxKind::Identifier { |
1239 | return false; |
1240 | } |
1241 | if parent.kind() == SyntaxKind::SubElement { |
1242 | return true; |
1243 | }; |
1244 | if parent.kind() == SyntaxKind::QualifiedName |
1245 | && matches!( |
1246 | parent.parent().map(|n| n.kind()), |
1247 | Some(SyntaxKind::Expression | SyntaxKind::StatePropertyChange) |
1248 | ) |
1249 | { |
1250 | let mut c = parent.children_with_tokens(); |
1251 | if let Some(NodeOrToken::Token(first)) = c.next() { |
1252 | return first.text_range() == tk.text_range() |
1253 | && matches!(c.next(), Some(NodeOrToken::Token(second)) if second.kind() == SyntaxKind::Dot); |
1254 | } |
1255 | } |
1256 | |
1257 | false |
1258 | } |
1259 | if is_element_id(token, parent) { |
1260 | // An id: search all use of the id in this Component |
1261 | let mut candidate = parent.parent(); |
1262 | while let Some(c) = candidate { |
1263 | if c.kind() == SyntaxKind::Component { |
1264 | let mut ranges = Vec::new(); |
1265 | let mut found_definition = false; |
1266 | recurse(&mut ranges, &mut found_definition, c, token.text()); |
1267 | fn recurse( |
1268 | ranges: &mut Vec<rowan::TextRange>, |
1269 | found_definition: &mut bool, |
1270 | c: SyntaxNode, |
1271 | text: &str, |
1272 | ) { |
1273 | for x in c.children_with_tokens() { |
1274 | match x { |
1275 | NodeOrToken::Node(n) => recurse(ranges, found_definition, n, text), |
1276 | NodeOrToken::Token(tk) => { |
1277 | if is_element_id(&tk, &c) && tk.text() == text { |
1278 | ranges.push(tk.text_range()); |
1279 | if c.kind() == SyntaxKind::SubElement { |
1280 | *found_definition = true; |
1281 | } |
1282 | } |
1283 | } |
1284 | } |
1285 | } |
1286 | } |
1287 | if !found_definition { |
1288 | return None; |
1289 | } |
1290 | return Some(ranges); |
1291 | } |
1292 | candidate = c.parent() |
1293 | } |
1294 | } |
1295 | None |
1296 | } |
1297 | |
1298 | pub async fn load_configuration(ctx: &Context) -> Result<()> { |
1299 | if !ctx |
1300 | .init_param |
1301 | .capabilities |
1302 | .workspace |
1303 | .as_ref() |
1304 | .and_then(|w| w.configuration) |
1305 | .unwrap_or(false) |
1306 | { |
1307 | return Ok(()); |
1308 | } |
1309 | |
1310 | let r = ctx |
1311 | .server_notifier |
1312 | .send_request::<lsp_types::request::WorkspaceConfiguration>( |
1313 | lsp_types::ConfigurationParams { |
1314 | items: vec![lsp_types::ConfigurationItem { |
1315 | scope_uri: None, |
1316 | section: Some("slint" .into()), |
1317 | }], |
1318 | }, |
1319 | )? |
1320 | .await?; |
1321 | |
1322 | let document_cache = &mut ctx.document_cache.borrow_mut(); |
1323 | let mut hide_ui = None; |
1324 | for v in r { |
1325 | if let Some(o) = v.as_object() { |
1326 | if let Some(ip) = o.get("includePaths" ).and_then(|v| v.as_array()) { |
1327 | if !ip.is_empty() { |
1328 | document_cache.documents.compiler_config.include_paths = |
1329 | ip.iter().filter_map(|x| x.as_str()).map(PathBuf::from).collect(); |
1330 | } |
1331 | } |
1332 | if let Some(lp) = o.get("libraryPaths" ).and_then(|v| v.as_object()) { |
1333 | if !lp.is_empty() { |
1334 | document_cache.documents.compiler_config.library_paths = lp |
1335 | .iter() |
1336 | .filter_map(|(k, v)| v.as_str().map(|v| (k.to_string(), PathBuf::from(v)))) |
1337 | .collect(); |
1338 | } |
1339 | } |
1340 | if let Some(style) = |
1341 | o.get("preview" ).and_then(|v| v.as_object()?.get("style" )?.as_str()) |
1342 | { |
1343 | if !style.is_empty() { |
1344 | document_cache.documents.compiler_config.style = Some(style.into()); |
1345 | } |
1346 | } |
1347 | hide_ui = o.get("preview" ).and_then(|v| v.as_object()?.get("hide_ui" )?.as_bool()); |
1348 | } |
1349 | } |
1350 | |
1351 | // Always load the widgets so we can auto-complete them |
1352 | let mut diag = BuildDiagnostics::default(); |
1353 | document_cache.documents.import_component("std-widgets.slint" , "StyleMetrics" , &mut diag).await; |
1354 | |
1355 | let cc = &document_cache.documents.compiler_config; |
1356 | let config = common::PreviewConfig { |
1357 | hide_ui, |
1358 | style: cc.style.clone().unwrap_or_default(), |
1359 | include_paths: cc.include_paths.clone(), |
1360 | library_paths: cc.library_paths.clone(), |
1361 | }; |
1362 | document_cache.preview_config = config.clone(); |
1363 | ctx.server_notifier |
1364 | .send_message_to_preview(common::LspToPreviewMessage::SetConfiguration { config }); |
1365 | Ok(()) |
1366 | } |
1367 | |
1368 | #[cfg (test)] |
1369 | pub mod tests { |
1370 | use super::*; |
1371 | |
1372 | use lsp_types::WorkspaceEdit; |
1373 | |
1374 | use test::{complex_document_cache, loaded_document_cache}; |
1375 | |
1376 | #[test ] |
1377 | fn test_reload_document_invalid_contents() { |
1378 | let (_, url, diag) = loaded_document_cache("This is not valid!" .into()); |
1379 | |
1380 | assert!(diag.len() == 1); // Only one URL is known |
1381 | |
1382 | let diagnostics = diag.get(&url).expect("URL not found in result" ); |
1383 | assert_eq!(diagnostics.len(), 1); |
1384 | assert_eq!(diagnostics[0].severity, Some(lsp_types::DiagnosticSeverity::ERROR)); |
1385 | } |
1386 | |
1387 | #[test ] |
1388 | fn test_reload_document_valid_contents() { |
1389 | let (_, url, diag) = |
1390 | loaded_document_cache(r#"export component Main inherits Rectangle { }"# .into()); |
1391 | |
1392 | assert!(diag.len() == 1); // Only one URL is known |
1393 | let diagnostics = diag.get(&url).expect("URL not found in result" ); |
1394 | assert!(diagnostics.is_empty()); |
1395 | } |
1396 | |
1397 | #[test ] |
1398 | fn test_text_document_color_no_color_set() { |
1399 | let (mut dc, uri, _) = loaded_document_cache( |
1400 | r#" |
1401 | component Main inherits Rectangle { } |
1402 | "# |
1403 | .into(), |
1404 | ); |
1405 | |
1406 | let result = get_document_color(&mut dc, &lsp_types::TextDocumentIdentifier { uri }) |
1407 | .expect("Color Vec was returned" ); |
1408 | assert!(result.is_empty()); |
1409 | } |
1410 | |
1411 | #[test ] |
1412 | fn test_text_document_color_rgba_color() { |
1413 | let (mut dc, uri, _) = loaded_document_cache( |
1414 | r#" |
1415 | component Main inherits Rectangle { |
1416 | background: #1200FF80; |
1417 | } |
1418 | "# |
1419 | .into(), |
1420 | ); |
1421 | |
1422 | let result = get_document_color(&mut dc, &lsp_types::TextDocumentIdentifier { uri }) |
1423 | .expect("Color Vec was returned" ); |
1424 | |
1425 | assert_eq!(result.len(), 1); |
1426 | |
1427 | let start = &result[0].range.start; |
1428 | assert_eq!(start.line, 2); |
1429 | assert_eq!(start.character, 28); // TODO: Why is this not 30? |
1430 | |
1431 | let end = &result[0].range.end; |
1432 | assert_eq!(end.line, 2); |
1433 | assert_eq!(end.character, 37); // TODO: Why is this not 39? |
1434 | |
1435 | let color = &result[0].color; |
1436 | assert_eq!(f64::trunc(color.red as f64 * 255.0), 18.0); |
1437 | assert_eq!(f64::trunc(color.green as f64 * 255.0), 0.0); |
1438 | assert_eq!(f64::trunc(color.blue as f64 * 255.0), 255.0); |
1439 | assert_eq!(f64::trunc(color.alpha as f64 * 255.0), 128.0); |
1440 | } |
1441 | |
1442 | fn id_at_position( |
1443 | dc: &mut DocumentCache, |
1444 | url: &Url, |
1445 | line: u32, |
1446 | character: u32, |
1447 | ) -> Option<String> { |
1448 | let result = element_at_position(&dc.documents, url, &Position { line, character })?; |
1449 | let element = result.element.borrow(); |
1450 | Some(element.id.clone()) |
1451 | } |
1452 | |
1453 | fn base_type_at_position( |
1454 | dc: &mut DocumentCache, |
1455 | url: &Url, |
1456 | line: u32, |
1457 | character: u32, |
1458 | ) -> Option<String> { |
1459 | let result = element_at_position(&dc.documents, url, &Position { line, character })?; |
1460 | let element = result.element.borrow(); |
1461 | Some(format!(" {}" , &element.base_type)) |
1462 | } |
1463 | |
1464 | #[test ] |
1465 | fn test_element_at_position_no_element() { |
1466 | let (mut dc, url, _) = complex_document_cache(); |
1467 | assert_eq!(id_at_position(&mut dc, &url, 0, 10), None); |
1468 | // TODO: This is past the end of the line and should thus return None |
1469 | assert_eq!(id_at_position(&mut dc, &url, 42, 90), Some(String::new())); |
1470 | assert_eq!(id_at_position(&mut dc, &url, 1, 0), None); |
1471 | assert_eq!(id_at_position(&mut dc, &url, 55, 1), None); |
1472 | assert_eq!(id_at_position(&mut dc, &url, 56, 5), None); |
1473 | } |
1474 | |
1475 | #[test ] |
1476 | fn test_element_at_position_no_such_document() { |
1477 | let (mut dc, _, _) = complex_document_cache(); |
1478 | assert_eq!( |
1479 | id_at_position(&mut dc, &Url::parse("https://foo.bar/baz" ).unwrap(), 5, 0), |
1480 | None |
1481 | ); |
1482 | } |
1483 | |
1484 | #[test ] |
1485 | fn test_element_at_position_root() { |
1486 | let (mut dc, url, _) = complex_document_cache(); |
1487 | |
1488 | assert_eq!(id_at_position(&mut dc, &url, 2, 30), Some("root" .to_string())); |
1489 | assert_eq!(id_at_position(&mut dc, &url, 2, 32), Some("root" .to_string())); |
1490 | assert_eq!(id_at_position(&mut dc, &url, 2, 42), Some("root" .to_string())); |
1491 | assert_eq!(id_at_position(&mut dc, &url, 3, 0), Some("root" .to_string())); |
1492 | assert_eq!(id_at_position(&mut dc, &url, 3, 53), Some("root" .to_string())); |
1493 | assert_eq!(id_at_position(&mut dc, &url, 4, 19), Some("root" .to_string())); |
1494 | assert_eq!(id_at_position(&mut dc, &url, 5, 0), Some("root" .to_string())); |
1495 | assert_eq!(id_at_position(&mut dc, &url, 6, 8), Some("root" .to_string())); |
1496 | assert_eq!(id_at_position(&mut dc, &url, 6, 15), Some("root" .to_string())); |
1497 | assert_eq!(id_at_position(&mut dc, &url, 6, 23), Some("root" .to_string())); |
1498 | assert_eq!(id_at_position(&mut dc, &url, 8, 15), Some("root" .to_string())); |
1499 | assert_eq!(id_at_position(&mut dc, &url, 12, 3), Some("root" .to_string())); // right before child // TODO: Seems wrong! |
1500 | assert_eq!(id_at_position(&mut dc, &url, 51, 5), Some("root" .to_string())); // right after child // TODO: Why does this not work? |
1501 | assert_eq!(id_at_position(&mut dc, &url, 52, 0), Some("root" .to_string())); |
1502 | } |
1503 | |
1504 | #[test ] |
1505 | fn test_element_at_position_child() { |
1506 | let (mut dc, url, _) = complex_document_cache(); |
1507 | |
1508 | assert_eq!(base_type_at_position(&mut dc, &url, 12, 4), Some("VerticalBox" .to_string())); |
1509 | assert_eq!(base_type_at_position(&mut dc, &url, 14, 22), Some("HorizontalBox" .to_string())); |
1510 | assert_eq!(base_type_at_position(&mut dc, &url, 15, 33), Some("Text" .to_string())); |
1511 | assert_eq!(base_type_at_position(&mut dc, &url, 27, 4), Some("VerticalBox" .to_string())); |
1512 | assert_eq!(base_type_at_position(&mut dc, &url, 28, 8), Some("Text" .to_string())); |
1513 | assert_eq!(base_type_at_position(&mut dc, &url, 51, 4), Some("VerticalBox" .to_string())); |
1514 | } |
1515 | |
1516 | #[test ] |
1517 | fn test_document_symbols() { |
1518 | let (mut dc, uri, _) = complex_document_cache(); |
1519 | |
1520 | let result = |
1521 | get_document_symbols(&mut dc, &lsp_types::TextDocumentIdentifier { uri }).unwrap(); |
1522 | |
1523 | if let DocumentSymbolResponse::Nested(result) = result { |
1524 | assert_eq!(result.len(), 1); |
1525 | |
1526 | let first = result.first().unwrap(); |
1527 | assert_eq!(&first.name, "MainWindow" ); |
1528 | } else { |
1529 | unreachable!(); |
1530 | } |
1531 | } |
1532 | |
1533 | #[test ] |
1534 | fn test_document_symbols_hello_world() { |
1535 | let (mut dc, uri, _) = loaded_document_cache( |
1536 | r#"import { Button, VerticalBox } from "std-widgets.slint"; |
1537 | component Demo { |
1538 | VerticalBox { |
1539 | alignment: start; |
1540 | Text { |
1541 | text: "Hello World!"; |
1542 | font-size: 24px; |
1543 | horizontal-alignment: center; |
1544 | } |
1545 | Image { |
1546 | source: @image-url("https://slint.dev/logo/slint-logo-full-light.svg"); |
1547 | height: 100px; |
1548 | } |
1549 | HorizontalLayout { alignment: center; Button { text: "OK!"; } } |
1550 | } |
1551 | } |
1552 | "# |
1553 | .into(), |
1554 | ); |
1555 | let result = |
1556 | get_document_symbols(&mut dc, &lsp_types::TextDocumentIdentifier { uri }).unwrap(); |
1557 | |
1558 | if let DocumentSymbolResponse::Nested(result) = result { |
1559 | assert_eq!(result.len(), 1); |
1560 | |
1561 | let first = result.first().unwrap(); |
1562 | assert_eq!(&first.name, "Demo" ); |
1563 | } else { |
1564 | unreachable!(); |
1565 | } |
1566 | } |
1567 | |
1568 | #[test ] |
1569 | fn test_document_symbols_no_empty_names() { |
1570 | // issue #3979 |
1571 | let (mut dc, uri, _) = loaded_document_cache( |
1572 | r#"import { Button, VerticalBox } from "std-widgets.slint"; |
1573 | struct Foo {} |
1574 | enum Bar {} |
1575 | export component Yo { Rectangle {} } |
1576 | export component {} |
1577 | struct {} |
1578 | enum {} |
1579 | "# |
1580 | .into(), |
1581 | ); |
1582 | let result = |
1583 | get_document_symbols(&mut dc, &lsp_types::TextDocumentIdentifier { uri }).unwrap(); |
1584 | |
1585 | if let DocumentSymbolResponse::Nested(result) = result { |
1586 | assert_eq!(result.len(), 3); |
1587 | assert_eq!(result[0].name, "Foo" ); |
1588 | assert_eq!(result[1].name, "Bar" ); |
1589 | assert_eq!(result[2].name, "Yo" ); |
1590 | } else { |
1591 | unreachable!(); |
1592 | } |
1593 | } |
1594 | |
1595 | #[test ] |
1596 | fn test_document_symbols_positions() { |
1597 | let source = r#"import { Button } from "std-widgets.slint"; |
1598 | |
1599 | enum TheEnum { |
1600 | Abc, Def |
1601 | }/*TheEnum*/ |
1602 | |
1603 | component FooBar { |
1604 | in property <TheEnum> the-enum; |
1605 | HorizontalLayout { |
1606 | btn := Button {} |
1607 | Rectangle { |
1608 | ta := TouchArea {} |
1609 | Image { |
1610 | } |
1611 | } |
1612 | }/*HorizontalLayout*/ |
1613 | }/*FooBar*/ |
1614 | |
1615 | struct Str { abc: string } |
1616 | |
1617 | export global SomeGlobal { |
1618 | in-out property<Str> prop; |
1619 | }/*SomeGlobal*/ |
1620 | |
1621 | export component TestWindow inherits Window { |
1622 | FooBar {} |
1623 | }/*TestWindow*/ |
1624 | "# ; |
1625 | |
1626 | let (mut dc, uri, _) = test::loaded_document_cache(source.into()); |
1627 | |
1628 | let result = |
1629 | get_document_symbols(&mut dc, &lsp_types::TextDocumentIdentifier { uri: uri.clone() }) |
1630 | .unwrap(); |
1631 | |
1632 | let check_start_with = |pos, str: &str| { |
1633 | let (_, offset) = get_document_and_offset(&dc.documents, &uri, &pos).unwrap(); |
1634 | assert_eq!(&source[offset as usize..][..str.len()], str); |
1635 | }; |
1636 | |
1637 | let DocumentSymbolResponse::Nested(result) = result else { |
1638 | panic!("not nested {result:?}" ) |
1639 | }; |
1640 | |
1641 | assert_eq!(result.len(), 5); |
1642 | assert_eq!(result[0].name, "TheEnum" ); |
1643 | check_start_with(result[0].range.start, "enum TheEnum {" ); |
1644 | check_start_with(result[0].range.end, "/*TheEnum*/" ); |
1645 | check_start_with(result[0].selection_range.start, "TheEnum {" ); |
1646 | check_start_with(result[0].selection_range.end, " {" ); |
1647 | assert_eq!(result[1].name, "FooBar" ); |
1648 | check_start_with(result[1].range.start, "component FooBar {" ); |
1649 | check_start_with(result[1].range.end, "/*FooBar*/" ); |
1650 | check_start_with(result[1].selection_range.start, "FooBar {" ); |
1651 | check_start_with(result[1].selection_range.end, " {" ); |
1652 | assert_eq!(result[2].name, "Str" ); |
1653 | check_start_with(result[2].range.start, "struct Str {" ); |
1654 | check_start_with(result[2].range.end, " \n" ); |
1655 | check_start_with(result[2].selection_range.start, "Str {" ); |
1656 | check_start_with(result[2].selection_range.end, " {" ); |
1657 | assert_eq!(result[3].name, "SomeGlobal" ); |
1658 | check_start_with(result[3].range.start, "global SomeGlobal" ); |
1659 | check_start_with(result[3].range.end, "/*SomeGlobal*/" ); |
1660 | check_start_with(result[3].selection_range.start, "SomeGlobal {" ); |
1661 | check_start_with(result[3].selection_range.end, " {" ); |
1662 | assert_eq!(result[4].name, "TestWindow" ); |
1663 | check_start_with(result[4].range.start, "component TestWindow inherits Window {" ); |
1664 | check_start_with(result[4].range.end, "/*TestWindow*/" ); |
1665 | check_start_with(result[4].selection_range.start, "TestWindow inherits" ); |
1666 | check_start_with(result[4].selection_range.end, " inherits" ); |
1667 | |
1668 | macro_rules! tree { |
1669 | ($root:literal $($more:literal)*) => { |
1670 | result[$root] $(.children.as_ref().unwrap()[$more])* |
1671 | }; |
1672 | } |
1673 | assert_eq!(tree!(1 0).name, "HorizontalLayout" ); |
1674 | check_start_with(tree!(1 0).range.start, "HorizontalLayout {" ); |
1675 | check_start_with(tree!(1 0).range.end, "/*HorizontalLayout*/" ); |
1676 | assert_eq!(tree!(1 0 0).name, "Button" ); |
1677 | assert_eq!(tree!(1 0 0).detail, Some("btn" .into())); |
1678 | check_start_with(tree!(1 0 0).range.start, "btn := Button" ); |
1679 | |
1680 | assert_eq!(tree!(1 0 1 0).name, "TouchArea" ); |
1681 | assert_eq!(tree!(1 0 1 0).detail, Some("ta" .into())); |
1682 | check_start_with(tree!(1 0 1 0).range.start, "ta := TouchArea" ); |
1683 | } |
1684 | |
1685 | #[test ] |
1686 | fn test_code_actions() { |
1687 | let (mut dc, url, _) = loaded_document_cache( |
1688 | r#"import { Button, VerticalBox, HorizontalBox} from "std-widgets.slint"; |
1689 | |
1690 | export component TestWindow inherits Window { |
1691 | VerticalBox { |
1692 | alignment: start; |
1693 | |
1694 | Text { |
1695 | text: "Hello World!"; |
1696 | font-size: 20px; |
1697 | } |
1698 | |
1699 | input := LineEdit { |
1700 | placeholder-text: "Enter your name"; |
1701 | } |
1702 | |
1703 | if (true): HorizontalBox { |
1704 | alignment: end; |
1705 | |
1706 | Button { text: "Cancel"; } |
1707 | |
1708 | Button { |
1709 | text: "OK"; |
1710 | primary: true; |
1711 | } |
1712 | } |
1713 | } |
1714 | }"# |
1715 | .into(), |
1716 | ); |
1717 | let mut capabilities = ClientCapabilities::default(); |
1718 | |
1719 | let text_literal = lsp_types::Range::new(Position::new(7, 18), Position::new(7, 32)); |
1720 | assert_eq!( |
1721 | token_descr(&mut dc, &url, &text_literal.start) |
1722 | .and_then(|(token, _)| get_code_actions(&mut dc, token, &capabilities)), |
1723 | Some(vec![CodeActionOrCommand::CodeAction(lsp_types::CodeAction { |
1724 | title: "Wrap in `@tr()`" .into(), |
1725 | edit: Some(WorkspaceEdit { |
1726 | document_changes: Some(lsp_types::DocumentChanges::Edits(vec![ |
1727 | lsp_types::TextDocumentEdit { |
1728 | text_document: lsp_types::OptionalVersionedTextDocumentIdentifier { |
1729 | version: Some(42), |
1730 | uri: url.clone(), |
1731 | }, |
1732 | edits: vec![ |
1733 | lsp_types::OneOf::Left(TextEdit::new( |
1734 | lsp_types::Range::new(text_literal.start, text_literal.start), |
1735 | "@tr(" .into() |
1736 | )), |
1737 | lsp_types::OneOf::Left(TextEdit::new( |
1738 | lsp_types::Range::new(text_literal.end, text_literal.end), |
1739 | ")" .into() |
1740 | )), |
1741 | ], |
1742 | } |
1743 | ])), |
1744 | ..Default::default() |
1745 | }), |
1746 | ..Default::default() |
1747 | })]), |
1748 | ); |
1749 | |
1750 | let text_element = lsp_types::Range::new(Position::new(6, 8), Position::new(9, 9)); |
1751 | for offset in 0..=4 { |
1752 | let pos = Position::new(text_element.start.line, text_element.start.character + offset); |
1753 | |
1754 | capabilities.experimental = None; |
1755 | assert_eq!( |
1756 | token_descr(&mut dc, &url, &pos).and_then(|(token, _)| get_code_actions( |
1757 | &mut dc, |
1758 | token, |
1759 | &capabilities |
1760 | )), |
1761 | None |
1762 | ); |
1763 | |
1764 | capabilities.experimental = Some(serde_json::json!({"snippetTextEdit" : true})); |
1765 | assert_eq!( |
1766 | token_descr(&mut dc, &url, &pos).and_then(|(token, _)| get_code_actions( |
1767 | &mut dc, |
1768 | token, |
1769 | &capabilities |
1770 | )), |
1771 | Some(vec![ |
1772 | CodeActionOrCommand::CodeAction(lsp_types::CodeAction { |
1773 | title: "Wrap in element" .into(), |
1774 | kind: Some(lsp_types::CodeActionKind::REFACTOR), |
1775 | edit: Some(WorkspaceEdit { |
1776 | document_changes: Some(lsp_types::DocumentChanges::Edits(vec![ |
1777 | lsp_types::TextDocumentEdit { |
1778 | text_document: |
1779 | lsp_types::OptionalVersionedTextDocumentIdentifier { |
1780 | version: Some(42), |
1781 | uri: url.clone(), |
1782 | }, |
1783 | edits: vec![lsp_types::OneOf::Left(TextEdit::new( |
1784 | text_element, |
1785 | r#"${0:element} { |
1786 | Text { |
1787 | text: "Hello World!"; |
1788 | font-size: 20px; |
1789 | } |
1790 | }"# |
1791 | .into() |
1792 | ))], |
1793 | }, |
1794 | ])), |
1795 | ..Default::default() |
1796 | }), |
1797 | ..Default::default() |
1798 | }), |
1799 | CodeActionOrCommand::CodeAction(lsp_types::CodeAction { |
1800 | title: "Repeat element" .into(), |
1801 | kind: Some(lsp_types::CodeActionKind::REFACTOR), |
1802 | edit: Some(WorkspaceEdit { |
1803 | document_changes: Some(lsp_types::DocumentChanges::Edits(vec![ |
1804 | lsp_types::TextDocumentEdit { |
1805 | text_document: |
1806 | lsp_types::OptionalVersionedTextDocumentIdentifier { |
1807 | version: Some(42), |
1808 | uri: url.clone(), |
1809 | }, |
1810 | edits: vec![lsp_types::OneOf::Left(TextEdit::new( |
1811 | lsp_types::Range::new( |
1812 | text_element.start, |
1813 | text_element.start |
1814 | ), |
1815 | r#"for ${1:name}[index] in ${0:model} : "# .into() |
1816 | ))], |
1817 | } |
1818 | ])), |
1819 | ..Default::default() |
1820 | }), |
1821 | ..Default::default() |
1822 | }), |
1823 | CodeActionOrCommand::CodeAction(lsp_types::CodeAction { |
1824 | title: "Make conditional" .into(), |
1825 | kind: Some(lsp_types::CodeActionKind::REFACTOR), |
1826 | edit: Some(WorkspaceEdit { |
1827 | document_changes: Some(lsp_types::DocumentChanges::Edits(vec![ |
1828 | lsp_types::TextDocumentEdit { |
1829 | text_document: |
1830 | lsp_types::OptionalVersionedTextDocumentIdentifier { |
1831 | version: Some(42), |
1832 | uri: url.clone(), |
1833 | }, |
1834 | edits: vec![lsp_types::OneOf::Left(TextEdit::new( |
1835 | lsp_types::Range::new( |
1836 | text_element.start, |
1837 | text_element.start |
1838 | ), |
1839 | r#"if ${0:condition} : "# .into() |
1840 | ))], |
1841 | } |
1842 | ])), |
1843 | ..Default::default() |
1844 | }), |
1845 | ..Default::default() |
1846 | }), |
1847 | ]) |
1848 | ); |
1849 | } |
1850 | |
1851 | let horizontal_box = lsp_types::Range::new(Position::new(15, 19), Position::new(24, 9)); |
1852 | |
1853 | capabilities.experimental = None; |
1854 | assert_eq!( |
1855 | token_descr(&mut dc, &url, &horizontal_box.start) |
1856 | .and_then(|(token, _)| get_code_actions(&mut dc, token, &capabilities)), |
1857 | None |
1858 | ); |
1859 | |
1860 | capabilities.experimental = Some(serde_json::json!({"snippetTextEdit" : true})); |
1861 | assert_eq!( |
1862 | token_descr(&mut dc, &url, &horizontal_box.start) |
1863 | .and_then(|(token, _)| get_code_actions(&mut dc, token, &capabilities)), |
1864 | Some(vec![ |
1865 | CodeActionOrCommand::CodeAction(lsp_types::CodeAction { |
1866 | title: "Wrap in element" .into(), |
1867 | kind: Some(lsp_types::CodeActionKind::REFACTOR), |
1868 | edit: Some(WorkspaceEdit { |
1869 | document_changes: Some(lsp_types::DocumentChanges::Edits(vec![ |
1870 | lsp_types::TextDocumentEdit { |
1871 | text_document: lsp_types::OptionalVersionedTextDocumentIdentifier { |
1872 | version: Some(42), |
1873 | uri: url.clone(), |
1874 | }, |
1875 | edits: vec![lsp_types::OneOf::Left(TextEdit::new( |
1876 | horizontal_box, |
1877 | r#"${0:element} { |
1878 | HorizontalBox { |
1879 | alignment: end; |
1880 | |
1881 | Button { text: "Cancel"; } |
1882 | |
1883 | Button { |
1884 | text: "OK"; |
1885 | primary: true; |
1886 | } |
1887 | } |
1888 | }"# |
1889 | .into() |
1890 | ))] |
1891 | } |
1892 | ])), |
1893 | ..Default::default() |
1894 | }), |
1895 | ..Default::default() |
1896 | }), |
1897 | CodeActionOrCommand::CodeAction(lsp_types::CodeAction { |
1898 | title: "Remove element" .into(), |
1899 | kind: Some(lsp_types::CodeActionKind::REFACTOR), |
1900 | edit: Some(WorkspaceEdit { |
1901 | document_changes: Some(lsp_types::DocumentChanges::Edits(vec![ |
1902 | lsp_types::TextDocumentEdit { |
1903 | text_document: lsp_types::OptionalVersionedTextDocumentIdentifier { |
1904 | version: Some(42), |
1905 | uri: url.clone(), |
1906 | }, |
1907 | edits: vec![lsp_types::OneOf::Left(TextEdit::new( |
1908 | horizontal_box, |
1909 | r#"Button { text: "Cancel"; } |
1910 | |
1911 | Button { |
1912 | text: "OK"; |
1913 | primary: true; |
1914 | }"# |
1915 | .into() |
1916 | ))] |
1917 | } |
1918 | ])), |
1919 | ..Default::default() |
1920 | }), |
1921 | ..Default::default() |
1922 | }), |
1923 | ]) |
1924 | ); |
1925 | |
1926 | let line_edit = Position::new(11, 20); |
1927 | let import_pos = lsp_types::Position::new(0, 43); |
1928 | capabilities.experimental = None; |
1929 | assert_eq!( |
1930 | token_descr(&mut dc, &url, &line_edit).and_then(|(token, _)| get_code_actions( |
1931 | &mut dc, |
1932 | token, |
1933 | &capabilities |
1934 | )), |
1935 | Some(vec![CodeActionOrCommand::CodeAction(lsp_types::CodeAction { |
1936 | title: "Add import from \"std-widgets.slint \"" .into(), |
1937 | kind: Some(lsp_types::CodeActionKind::QUICKFIX), |
1938 | edit: Some(WorkspaceEdit { |
1939 | document_changes: Some(lsp_types::DocumentChanges::Edits(vec![ |
1940 | lsp_types::TextDocumentEdit { |
1941 | text_document: lsp_types::OptionalVersionedTextDocumentIdentifier { |
1942 | version: Some(42), |
1943 | uri: url.clone(), |
1944 | }, |
1945 | edits: vec![lsp_types::OneOf::Left(TextEdit::new( |
1946 | lsp_types::Range::new(import_pos, import_pos), |
1947 | ", LineEdit" .into() |
1948 | ))] |
1949 | } |
1950 | ])), |
1951 | ..Default::default() |
1952 | }), |
1953 | ..Default::default() |
1954 | }),]) |
1955 | ); |
1956 | } |
1957 | } |
1958 | |