diff --git a/tools/lsp/main.rs b/tools/lsp/main.rs index 84e8c302b..8fdc9df68 100644 --- a/tools/lsp/main.rs +++ b/tools/lsp/main.rs @@ -16,11 +16,12 @@ use std::path::Path; use lsp_server::{Connection, Message, Request, RequestId, Response}; use lsp_types::notification::{DidChangeTextDocument, DidOpenTextDocument, Notification}; -use lsp_types::request::GotoDefinition; +use lsp_types::request::{CodeActionRequest, ExecuteCommand, GotoDefinition}; use lsp_types::request::{Completion, HoverRequest}; use lsp_types::{ - CompletionItem, CompletionItemKind, CompletionOptions, DidChangeTextDocumentParams, - DidOpenTextDocumentParams, GotoDefinitionResponse, Hover, HoverProviderCapability, + CodeActionOrCommand, CodeActionProviderCapability, Command, CompletionItem, CompletionItemKind, + CompletionOptions, DidChangeTextDocumentParams, DidOpenTextDocumentParams, + ExecuteCommandOptions, GotoDefinitionResponse, Hover, HoverProviderCapability, InitializeParams, LocationLink, OneOf, Position, PublishDiagnosticsParams, Range, ServerCapabilities, TextDocumentSyncCapability, Url, WorkDoneProgressOptions, }; @@ -33,6 +34,8 @@ use sixtyfps_compilerlib::{object_tree, CompilerConfiguration}; type Error = Box; +const SHOW_PREVIEW_COMMAND: &str = "showPreview"; + struct DocumentCache<'a> { documents: TypeLoader<'a>, newline_offsets: HashMap>, @@ -76,7 +79,11 @@ fn run_lsp_server() -> Result<(), Error> { text_document_sync: Some(TextDocumentSyncCapability::Kind( lsp_types::TextDocumentSyncKind::Full, )), - + code_action_provider: Some(CodeActionProviderCapability::Simple(true)), + execute_command_provider: Some(ExecuteCommandOptions { + commands: vec![SHOW_PREVIEW_COMMAND.into()], + ..Default::default() + }), ..ServerCapabilities::default() }; let server_capabilities = serde_json::to_value(&capabilities).unwrap(); @@ -118,13 +125,21 @@ fn handle_request( ) -> Result<(), Error> { let mut req = Some(req); if let Some((id, params)) = cast::(&mut req) { - let result = token_descr(document_cache, params.text_document_position_params) - .and_then(|token| goto_definition(document_cache, token.parent())); + let result = token_descr( + document_cache, + params.text_document_position_params.text_document, + params.text_document_position_params.position, + ) + .and_then(|token| goto_definition(document_cache, token.parent())); let resp = Response::new_ok(id, result); connection.sender.send(Message::Response(resp))?; } else if let Some((id, params)) = cast::(&mut req) { - let result = token_descr(document_cache, params.text_document_position) - .and_then(|token| completion_at(document_cache, token.parent())); + let result = token_descr( + document_cache, + params.text_document_position.text_document, + params.text_document_position.position, + ) + .and_then(|token| completion_at(document_cache, token.parent())); let resp = Response::new_ok(id, result); connection.sender.send(Message::Response(resp))?; } else if let Some((id, _params)) = cast::(&mut req) { @@ -139,6 +154,18 @@ fn handle_request( let resp = Response::new_ok(id, result); connection.sender.send(Message::Response(resp))?;*/ connection.sender.send(Message::Response(Response::new_ok(id, None::)))?; + } else if let Some((id, params)) = cast::(&mut req) { + let result = token_descr(document_cache, params.text_document, params.range.start) + .and_then(|token| get_code_actions(document_cache, token.parent())); + connection.sender.send(Message::Response(Response::new_ok(id, result)))?; + } else if let Some((id, params)) = cast::(&mut req) { + match params.command.as_str() { + SHOW_PREVIEW_COMMAND => show_preview_command(¶ms.arguments, connection)?, + _ => (), + } + connection + .sender + .send(Message::Response(Response::new_ok(id, None::)))?; }; Ok(()) } @@ -180,24 +207,34 @@ fn handle_notification( )?; } "sixtyfps/showPreview" => { - let e = || -> Error { "InvalidParameter".into() }; - let path = if let serde_json::Value::String(s) = req.params.get(0).ok_or_else(e)? { - std::path::PathBuf::from(s) - } else { - return Err(e()); - }; - let path_canon = path.canonicalize().unwrap_or_else(|_| path.to_owned()); - preview::load_preview( - connection.sender.clone(), - path_canon.into(), - preview::PostLoadBehavior::ShowAfterLoad, - ); + show_preview_command(req.params.as_array().map_or(&[], |x| x.as_slice()), connection)?; } _ => (), } Ok(()) } +fn show_preview_command( + params: &[serde_json::Value], + connection: &Connection, +) -> Result<(), Error> { + let e = || -> Error { "InvalidParameter".into() }; + let path = if let serde_json::Value::String(s) = params.get(0).ok_or_else(e)? { + std::path::PathBuf::from(s) + } else { + return Err(e()); + }; + let path_canon = path.canonicalize().unwrap_or_else(|_| path.to_owned()); + let component = params.get(1).and_then(|v| v.as_str()).map(|v| v.to_string()); + preview::load_preview( + connection.sender.clone(), + path_canon.into(), + component, + preview::PostLoadBehavior::ShowAfterLoad, + ); + Ok(()) +} + fn newline_offsets_from_content(content: &str) -> Vec { let mut ln_offs = 0; content @@ -279,18 +316,14 @@ fn to_range(span: (usize, usize)) -> Range { } fn token_descr( - document_cache: &DocumentCache, - lsp_position: lsp_types::TextDocumentPositionParams, + document_cache: &mut DocumentCache, + text_document: lsp_types::TextDocumentIdentifier, + pos: Position, ) -> Option { - let o = document_cache - .newline_offsets - .get(&lsp_position.text_document.uri)? - .get(lsp_position.position.line as usize)? - + lsp_position.position.character as u32; + let o = document_cache.newline_offsets.get(&text_document.uri)?.get(pos.line as usize)? + + pos.character as u32; - let doc = document_cache - .documents - .get_document(&lsp_position.text_document.uri.to_file_path().ok()?)?; + let doc = document_cache.documents.get_document(&text_document.uri.to_file_path().ok()?)?; let node = doc.node.as_ref()?; if !node.text_range().contains(o.into()) { return None; @@ -379,6 +412,34 @@ fn goto_node( }])) } +fn get_code_actions( + _document_cache: &mut DocumentCache, + node: SyntaxNode, +) -> Option> { + let component = syntax_nodes::Component::new(node.clone()) + .or_else(|| { + syntax_nodes::DeclaredIdentifier::new(node.clone()) + .and_then(|n| n.parent()) + .and_then(|p| syntax_nodes::Component::new(p)) + }) + .or_else(|| { + syntax_nodes::QualifiedName::new(node.clone()) + .and_then(|n| n.parent()) + .and_then(|p| syntax_nodes::Element::new(p)) + .and_then(|n| n.parent()) + .and_then(|p| syntax_nodes::Component::new(p)) + })?; + + let component_name = + sixtyfps_compilerlib::parser::identifier_text(&component.DeclaredIdentifier())?; + + Some(vec![CodeActionOrCommand::Command(Command::new( + "Show preview".into(), + SHOW_PREVIEW_COMMAND.into(), + Some(vec![component.source_file.path().to_string_lossy().into(), component_name.into()]), + ))]) +} + fn completion_at(document_cache: &DocumentCache, token: SyntaxNode) -> Option> { match token.kind() { SyntaxKind::Element => { diff --git a/tools/lsp/preview.rs b/tools/lsp/preview.rs index a024302b4..46bf8b6e7 100644 --- a/tools/lsp/preview.rs +++ b/tools/lsp/preview.rs @@ -68,11 +68,12 @@ pub enum PostLoadBehavior { pub fn load_preview( sender: crossbeam_channel::Sender, path: std::path::PathBuf, + component: Option, post_load_behavior: PostLoadBehavior, ) { - run_in_ui_thread(Box::pin( - async move { reload_preview(sender, &path, post_load_behavior).await }, - )); + run_in_ui_thread(Box::pin(async move { + reload_preview(sender, &path, component, post_load_behavior).await + })); } #[derive(Default)] @@ -80,6 +81,7 @@ struct ContentCache { source_code: HashMap, dependency: HashSet, current_root: PathBuf, + current_component: Option, sender: Option>, } @@ -91,10 +93,11 @@ pub fn set_contents(path: &Path, content: String) { cache.source_code.insert(path.to_owned(), content); if cache.dependency.contains(path) { let current_root = cache.current_root.clone(); + let current_component = cache.current_component.clone(); let sender = cache.sender.take(); drop(cache); if let Some(sender) = sender { - load_preview(sender, current_root, PostLoadBehavior::DoNothing); + load_preview(sender, current_root, current_component, PostLoadBehavior::DoNothing); } } } @@ -111,6 +114,7 @@ fn get_file_from_cache(path: PathBuf) -> Option { async fn reload_preview( sender: crossbeam_channel::Sender, path: &std::path::Path, + component: Option, post_load_behavior: PostLoadBehavior, ) { send_notification(&sender, "Loading Preview…", Health::Ok); @@ -127,7 +131,19 @@ async fn reload_preview( Box::pin(async move { get_file_from_cache(path).map(Result::Ok) }) }); - let compiled = if let Some(from_cache) = get_file_from_cache(path.to_owned()) { + let compiled = if let Some(mut from_cache) = get_file_from_cache(path.to_owned()) { + if let Some(component) = component { + from_cache = format!( + r#"{} +_Preview := Window {{ + {} {{ + width <=> root.width; + height <=> root.height; + }} +}}"#, + from_cache, component + ); + } builder.build_from_source(from_cache, path.to_owned()).await } else { builder.build_from_path(path).await