mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-01 14:21:16 +00:00
LSP: code action to preview a particular component
This commit is contained in:
parent
ea5e611f41
commit
bcce36385d
2 changed files with 112 additions and 35 deletions
|
@ -16,11 +16,12 @@ use std::path::Path;
|
||||||
|
|
||||||
use lsp_server::{Connection, Message, Request, RequestId, Response};
|
use lsp_server::{Connection, Message, Request, RequestId, Response};
|
||||||
use lsp_types::notification::{DidChangeTextDocument, DidOpenTextDocument, Notification};
|
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::request::{Completion, HoverRequest};
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
CompletionItem, CompletionItemKind, CompletionOptions, DidChangeTextDocumentParams,
|
CodeActionOrCommand, CodeActionProviderCapability, Command, CompletionItem, CompletionItemKind,
|
||||||
DidOpenTextDocumentParams, GotoDefinitionResponse, Hover, HoverProviderCapability,
|
CompletionOptions, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
|
||||||
|
ExecuteCommandOptions, GotoDefinitionResponse, Hover, HoverProviderCapability,
|
||||||
InitializeParams, LocationLink, OneOf, Position, PublishDiagnosticsParams, Range,
|
InitializeParams, LocationLink, OneOf, Position, PublishDiagnosticsParams, Range,
|
||||||
ServerCapabilities, TextDocumentSyncCapability, Url, WorkDoneProgressOptions,
|
ServerCapabilities, TextDocumentSyncCapability, Url, WorkDoneProgressOptions,
|
||||||
};
|
};
|
||||||
|
@ -33,6 +34,8 @@ use sixtyfps_compilerlib::{object_tree, CompilerConfiguration};
|
||||||
|
|
||||||
type Error = Box<dyn std::error::Error>;
|
type Error = Box<dyn std::error::Error>;
|
||||||
|
|
||||||
|
const SHOW_PREVIEW_COMMAND: &str = "showPreview";
|
||||||
|
|
||||||
struct DocumentCache<'a> {
|
struct DocumentCache<'a> {
|
||||||
documents: TypeLoader<'a>,
|
documents: TypeLoader<'a>,
|
||||||
newline_offsets: HashMap<Url, Vec<u32>>,
|
newline_offsets: HashMap<Url, Vec<u32>>,
|
||||||
|
@ -76,7 +79,11 @@ fn run_lsp_server() -> Result<(), Error> {
|
||||||
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
||||||
lsp_types::TextDocumentSyncKind::Full,
|
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()
|
..ServerCapabilities::default()
|
||||||
};
|
};
|
||||||
let server_capabilities = serde_json::to_value(&capabilities).unwrap();
|
let server_capabilities = serde_json::to_value(&capabilities).unwrap();
|
||||||
|
@ -118,12 +125,20 @@ fn handle_request(
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut req = Some(req);
|
let mut req = Some(req);
|
||||||
if let Some((id, params)) = cast::<GotoDefinition>(&mut req) {
|
if let Some((id, params)) = cast::<GotoDefinition>(&mut req) {
|
||||||
let result = token_descr(document_cache, params.text_document_position_params)
|
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()));
|
.and_then(|token| goto_definition(document_cache, token.parent()));
|
||||||
let resp = Response::new_ok(id, result);
|
let resp = Response::new_ok(id, result);
|
||||||
connection.sender.send(Message::Response(resp))?;
|
connection.sender.send(Message::Response(resp))?;
|
||||||
} else if let Some((id, params)) = cast::<Completion>(&mut req) {
|
} else if let Some((id, params)) = cast::<Completion>(&mut req) {
|
||||||
let result = token_descr(document_cache, params.text_document_position)
|
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()));
|
.and_then(|token| completion_at(document_cache, token.parent()));
|
||||||
let resp = Response::new_ok(id, result);
|
let resp = Response::new_ok(id, result);
|
||||||
connection.sender.send(Message::Response(resp))?;
|
connection.sender.send(Message::Response(resp))?;
|
||||||
|
@ -139,6 +154,18 @@ fn handle_request(
|
||||||
let resp = Response::new_ok(id, result);
|
let resp = Response::new_ok(id, result);
|
||||||
connection.sender.send(Message::Response(resp))?;*/
|
connection.sender.send(Message::Response(resp))?;*/
|
||||||
connection.sender.send(Message::Response(Response::new_ok(id, None::<Hover>)))?;
|
connection.sender.send(Message::Response(Response::new_ok(id, None::<Hover>)))?;
|
||||||
|
} else if let Some((id, params)) = cast::<CodeActionRequest>(&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::<ExecuteCommand>(&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::<serde_json::Value>)))?;
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -180,21 +207,31 @@ fn handle_notification(
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
"sixtyfps/showPreview" => {
|
"sixtyfps/showPreview" => {
|
||||||
|
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 e = || -> Error { "InvalidParameter".into() };
|
||||||
let path = if let serde_json::Value::String(s) = req.params.get(0).ok_or_else(e)? {
|
let path = if let serde_json::Value::String(s) = params.get(0).ok_or_else(e)? {
|
||||||
std::path::PathBuf::from(s)
|
std::path::PathBuf::from(s)
|
||||||
} else {
|
} else {
|
||||||
return Err(e());
|
return Err(e());
|
||||||
};
|
};
|
||||||
let path_canon = path.canonicalize().unwrap_or_else(|_| path.to_owned());
|
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(
|
preview::load_preview(
|
||||||
connection.sender.clone(),
|
connection.sender.clone(),
|
||||||
path_canon.into(),
|
path_canon.into(),
|
||||||
|
component,
|
||||||
preview::PostLoadBehavior::ShowAfterLoad,
|
preview::PostLoadBehavior::ShowAfterLoad,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,18 +316,14 @@ fn to_range(span: (usize, usize)) -> Range {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn token_descr(
|
fn token_descr(
|
||||||
document_cache: &DocumentCache,
|
document_cache: &mut DocumentCache,
|
||||||
lsp_position: lsp_types::TextDocumentPositionParams,
|
text_document: lsp_types::TextDocumentIdentifier,
|
||||||
|
pos: Position,
|
||||||
) -> Option<sixtyfps_compilerlib::parser::SyntaxToken> {
|
) -> Option<sixtyfps_compilerlib::parser::SyntaxToken> {
|
||||||
let o = document_cache
|
let o = document_cache.newline_offsets.get(&text_document.uri)?.get(pos.line as usize)?
|
||||||
.newline_offsets
|
+ pos.character as u32;
|
||||||
.get(&lsp_position.text_document.uri)?
|
|
||||||
.get(lsp_position.position.line as usize)?
|
|
||||||
+ lsp_position.position.character as u32;
|
|
||||||
|
|
||||||
let doc = document_cache
|
let doc = document_cache.documents.get_document(&text_document.uri.to_file_path().ok()?)?;
|
||||||
.documents
|
|
||||||
.get_document(&lsp_position.text_document.uri.to_file_path().ok()?)?;
|
|
||||||
let node = doc.node.as_ref()?;
|
let node = doc.node.as_ref()?;
|
||||||
if !node.text_range().contains(o.into()) {
|
if !node.text_range().contains(o.into()) {
|
||||||
return None;
|
return None;
|
||||||
|
@ -379,6 +412,34 @@ fn goto_node(
|
||||||
}]))
|
}]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_code_actions(
|
||||||
|
_document_cache: &mut DocumentCache,
|
||||||
|
node: SyntaxNode,
|
||||||
|
) -> Option<Vec<CodeActionOrCommand>> {
|
||||||
|
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<Vec<CompletionItem>> {
|
fn completion_at(document_cache: &DocumentCache, token: SyntaxNode) -> Option<Vec<CompletionItem>> {
|
||||||
match token.kind() {
|
match token.kind() {
|
||||||
SyntaxKind::Element => {
|
SyntaxKind::Element => {
|
||||||
|
|
|
@ -68,11 +68,12 @@ pub enum PostLoadBehavior {
|
||||||
pub fn load_preview(
|
pub fn load_preview(
|
||||||
sender: crossbeam_channel::Sender<Message>,
|
sender: crossbeam_channel::Sender<Message>,
|
||||||
path: std::path::PathBuf,
|
path: std::path::PathBuf,
|
||||||
|
component: Option<String>,
|
||||||
post_load_behavior: PostLoadBehavior,
|
post_load_behavior: PostLoadBehavior,
|
||||||
) {
|
) {
|
||||||
run_in_ui_thread(Box::pin(
|
run_in_ui_thread(Box::pin(async move {
|
||||||
async move { reload_preview(sender, &path, post_load_behavior).await },
|
reload_preview(sender, &path, component, post_load_behavior).await
|
||||||
));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -80,6 +81,7 @@ struct ContentCache {
|
||||||
source_code: HashMap<PathBuf, String>,
|
source_code: HashMap<PathBuf, String>,
|
||||||
dependency: HashSet<PathBuf>,
|
dependency: HashSet<PathBuf>,
|
||||||
current_root: PathBuf,
|
current_root: PathBuf,
|
||||||
|
current_component: Option<String>,
|
||||||
sender: Option<crossbeam_channel::Sender<Message>>,
|
sender: Option<crossbeam_channel::Sender<Message>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,10 +93,11 @@ pub fn set_contents(path: &Path, content: String) {
|
||||||
cache.source_code.insert(path.to_owned(), content);
|
cache.source_code.insert(path.to_owned(), content);
|
||||||
if cache.dependency.contains(path) {
|
if cache.dependency.contains(path) {
|
||||||
let current_root = cache.current_root.clone();
|
let current_root = cache.current_root.clone();
|
||||||
|
let current_component = cache.current_component.clone();
|
||||||
let sender = cache.sender.take();
|
let sender = cache.sender.take();
|
||||||
drop(cache);
|
drop(cache);
|
||||||
if let Some(sender) = sender {
|
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<String> {
|
||||||
async fn reload_preview(
|
async fn reload_preview(
|
||||||
sender: crossbeam_channel::Sender<Message>,
|
sender: crossbeam_channel::Sender<Message>,
|
||||||
path: &std::path::Path,
|
path: &std::path::Path,
|
||||||
|
component: Option<String>,
|
||||||
post_load_behavior: PostLoadBehavior,
|
post_load_behavior: PostLoadBehavior,
|
||||||
) {
|
) {
|
||||||
send_notification(&sender, "Loading Preview…", Health::Ok);
|
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) })
|
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
|
builder.build_from_source(from_cache, path.to_owned()).await
|
||||||
} else {
|
} else {
|
||||||
builder.build_from_path(path).await
|
builder.build_from_path(path).await
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue