LSP: add a setting to change the style

... and the include paths
This commit is contained in:
Olivier Goffart 2022-11-11 14:53:40 +01:00 committed by Olivier Goffart
parent 8f29f22c6c
commit cbe2d6d631
9 changed files with 260 additions and 37 deletions

View file

@ -78,6 +78,17 @@
"type": "string"
},
"description": "The command line arguments passed to the Slint LSP server"
},
"slint.preview.style": {
"type": "string",
"description": "The style to be used for the preview (eg: 'fluent', 'material', or 'native')"
},
"slint.includePaths": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of paths in which the `import` statement and `@image-url` are looked up"
}
}
},

View file

@ -26,8 +26,13 @@ slint_init(slint_wasm_data).then((_) => {
return new TextDecoder().decode(contents);
}
async function send_request(method: string, params: any): Promise<any> {
return await connection.sendRequest(method, params);
}
connection.onInitialize((params: InitializeParams): InitializeResult => {
the_lsp = slint_lsp.create(params, send_notification, load_file);
the_lsp = slint_lsp.create(params, send_notification, send_request, load_file);
return the_lsp.server_initialize_result();
});
@ -39,6 +44,10 @@ slint_init(slint_wasm_data).then((_) => {
return await the_lsp.handle_request(token, method, params);
});
connection.onDidChangeConfiguration(async (_) => {
the_lsp.reload_config();
});
connection.onDidChangeTextDocument(async (param) => {
await the_lsp.reload_document(param.contentChanges[param.contentChanges.length - 1].text, param.textDocument.uri, param.textDocument.version);
});

View file

@ -46,6 +46,15 @@ function startClient(context: vscode.ExtensionContext) {
return;
});
//client.onNotification(serverStatus, (params) => setServerStatus(params, statusBar));
vscode.workspace.onDidChangeConfiguration(async (ev) => {
if (ev.affectsConfiguration("slint")) {
await client.sendNotification("workspace/didChangeConfiguration", { settings: "" });
if (previewUrl) {
reload_preview(previewUrl, await getDocumentSource(previewUrl), previewComponent);
}
}
});
});
}
};
@ -66,12 +75,14 @@ function reload_preview(url: string, content: string, component: string) {
previewAccessedFiles.clear();
let webview_uri = previewPanel.webview.asWebviewUri(Uri.parse(url)).toString();
previewAccessedFiles.add(webview_uri);
const style = vscode.workspace.getConfiguration('slint').get<[string]>('preview.style');
const msg = {
command: "preview",
base_url: url,
webview_uri: webview_uri,
component: component,
content: content
content: content,
style: style,
};
if (previewBusy) {
queuedPreviewMsg = msg;
@ -179,8 +190,10 @@ function getPreviewHtml(slint_wasm_interpreter_url: Uri): string {
return from_editor || await (await fetch(url)).text();
}
async function render(source, base_url) {
let { component, error_string } = await slint.compile_from_string(source, base_url, async(url) => await load_file(url));
async function render(source, base_url, style) {
let { component, error_string } =
style ? await slint.compile_from_string_with_style(source, base_url, style, async(url) => await load_file(url))
: await slint.compile_from_string(source, base_url, async(url) => await load_file(url));
if (error_string != "") {
var text = document.createTextNode(error_string);
var p = document.createElement('pre');
@ -203,7 +216,7 @@ function getPreviewHtml(slint_wasm_interpreter_url: Uri): string {
window.addEventListener('message', async event => {
if (event.data.command === "preview") {
vscode.setState({base_url: event.data.base_url, component: event.data.component});
await render(event.data.content, event.data.webview_uri);
await render(event.data.content, event.data.webview_uri, event.data.style);
} else if (event.data.command === "file_loaded") {
let resolve = promises[event.data.url];
if (resolve) {

View file

@ -165,6 +165,12 @@ export function activate(context: vscode.ExtensionContext) {
const properties_provider = new PropertiesViewProvider(context.extensionUri);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(PropertiesViewProvider.viewType, properties_provider));
vscode.workspace.onDidChangeConfiguration(async (ev) => {
if (ev.affectsConfiguration("slint")) {
await client?.sendNotification("workspace/didChangeConfiguration", { settings: "" });
}
});
}
export function deactivate(): Thenable<void> | undefined {

View file

@ -16,12 +16,16 @@ mod test;
mod util;
use i_slint_compiler::CompilerConfiguration;
use lsp_types::notification::{DidChangeTextDocument, DidOpenTextDocument, Notification};
use lsp_types::notification::{
DidChangeConfiguration, DidChangeTextDocument, DidOpenTextDocument, Notification,
};
use lsp_types::{DidChangeTextDocumentParams, DidOpenTextDocumentParams, InitializeParams};
use server_loop::*;
use clap::Parser;
use lsp_server::{Connection, Message, Request, Response};
use lsp_server::{Connection, Message, RequestId, Response};
use std::collections::HashMap;
use std::sync::{atomic, Arc, Mutex};
#[derive(Clone, clap::Parser)]
#[command(author, version, about, long_about = None)]
@ -43,8 +47,15 @@ struct Cli {
backend: String,
}
type OutgoingRequestQueue = Arc<
Mutex<HashMap<RequestId, Box<dyn FnOnce(lsp_server::Response, &mut DocumentCache) + Send>>>,
>;
/// A handle that can be used to communicate with the client
///
/// This type is duplicated, with the same interface, in wasm_main.rs
#[derive(Clone)]
pub struct ServerNotifier(crossbeam_channel::Sender<Message>);
pub struct ServerNotifier(crossbeam_channel::Sender<Message>, OutgoingRequestQueue);
impl ServerNotifier {
pub fn send_notification(
&self,
@ -54,9 +65,35 @@ impl ServerNotifier {
self.0.send(Message::Notification(lsp_server::Notification::new(method, params)))?;
Ok(())
}
pub fn send_request<T: lsp_types::request::Request>(
&self,
request: T::Params,
f: impl FnOnce(Result<T::Result, String>, &mut DocumentCache) + Send + 'static,
) -> Result<(), Error> {
static REQ_ID: atomic::AtomicI32 = atomic::AtomicI32::new(0);
let id = RequestId::from(REQ_ID.fetch_add(1, atomic::Ordering::Relaxed));
let msg =
Message::Request(lsp_server::Request::new(id.clone(), T::METHOD.to_string(), request));
self.0.send(msg)?;
self.1.lock().unwrap().insert(
id,
Box::new(move |r, c| {
if let Some(r) = r.result {
f(serde_json::from_value(r).map_err(|e| e.to_string()), c)
} else if let Some(r) = r.error {
f(Err(r.message), c)
}
}),
);
Ok(())
}
}
pub struct RequestHolder(Request, crossbeam_channel::Sender<Message>);
/// The interface for a request received from the client
///
/// This type is duplicated, with the same interface, in wasm_main.rs
pub struct RequestHolder(lsp_server::Request, ServerNotifier);
impl RequestHolder {
pub fn handle_request<
Kind: lsp_types::request::Request,
@ -76,9 +113,9 @@ impl RequestHolder {
};
match f(param) {
Ok(r) => self.1.send(Message::Response(Response::new_ok(id, r)))?,
Ok(r) => self.1 .0.send(Message::Response(Response::new_ok(id, r)))?,
Err(e) => {
self.1.send(Message::Response(Response::new_err(id, 23, format!("{}", e))))?
self.1 .0.send(Message::Response(Response::new_err(id, 23, format!("{}", e))))?
}
};
@ -86,7 +123,7 @@ impl RequestHolder {
}
pub fn server_notifier(&self) -> ServerNotifier {
ServerNotifier(self.1.clone())
self.1.clone()
}
}
@ -151,6 +188,12 @@ fn main_loop(connection: &Connection, params: serde_json::Value) -> Result<(), E
compiler_config.include_paths = cli_args.include_paths;
let mut document_cache = DocumentCache::new(compiler_config);
let request_queue = OutgoingRequestQueue::default();
let server_notifier = ServerNotifier(connection.sender.clone(), request_queue.clone());
load_configuration(&server_notifier)?;
for msg in &connection.receiver {
match msg {
Message::Request(req) => {
@ -158,14 +201,21 @@ fn main_loop(connection: &Connection, params: serde_json::Value) -> Result<(), E
return Ok(());
}
handle_request(
RequestHolder(req, connection.sender.clone()),
RequestHolder(req, server_notifier.clone()),
&params,
&mut document_cache,
)?;
}
Message::Response(_resp) => {}
Message::Response(resp) => {
let f = request_queue
.lock()
.unwrap()
.remove(&resp.id)
.ok_or("Response to unknown request")?;
f(resp, &mut document_cache);
}
Message::Notification(notification) => {
handle_notification(connection, notification, &mut document_cache)?
handle_notification(&server_notifier, notification, &mut document_cache)?
}
}
}
@ -173,7 +223,7 @@ fn main_loop(connection: &Connection, params: serde_json::Value) -> Result<(), E
}
pub fn handle_notification(
connection: &Connection,
connection: &ServerNotifier,
req: lsp_server::Notification,
document_cache: &mut DocumentCache,
) -> Result<(), Error> {
@ -181,7 +231,7 @@ pub fn handle_notification(
DidOpenTextDocument::METHOD => {
let params: DidOpenTextDocumentParams = serde_json::from_value(req.params)?;
spin_on::spin_on(reload_document(
&ServerNotifier(connection.sender.clone()),
connection,
params.text_document.text,
params.text_document.uri,
params.text_document.version,
@ -191,19 +241,23 @@ pub fn handle_notification(
DidChangeTextDocument::METHOD => {
let mut params: DidChangeTextDocumentParams = serde_json::from_value(req.params)?;
spin_on::spin_on(reload_document(
&ServerNotifier(connection.sender.clone()),
connection,
params.content_changes.pop().unwrap().text,
params.text_document.uri,
params.text_document.version,
document_cache,
))?;
}
DidChangeConfiguration::METHOD => {
load_configuration(connection)?;
}
#[cfg(feature = "preview")]
"slint/showPreview" => {
show_preview_command(
req.params.as_array().map_or(&[], |x| x.as_slice()),
&ServerNotifier(connection.sender.clone()),
connection,
&document_cache.documents.compiler_config,
)?;
}
_ => (),

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
use crate::lsp_ext::{Health, ServerStatusNotification, ServerStatusParams};
use i_slint_compiler::CompilerConfiguration;
use lsp_types::notification::Notification;
use once_cell::sync::Lazy;
use slint_interpreter::ComponentHandle;
@ -152,6 +153,12 @@ pub struct PreviewComponent {
/// The name of the component within that file.
/// If None, then the last component is going to be shown.
pub component: Option<String>,
/// The list of include paths
pub include_paths: Vec<std::path::PathBuf>,
/// The style name for the preview
pub style: String,
}
#[derive(Default)]
@ -178,6 +185,23 @@ pub fn set_contents(path: &Path, content: String) {
}
}
pub fn config_changed(config: &CompilerConfiguration) {
if let Some(cache) = CONTENT_CACHE.get() {
let mut cache = cache.lock().unwrap();
let style = config.style.clone().unwrap_or_default();
if cache.current.style != style || cache.current.include_paths != config.include_paths {
cache.current.style = style;
cache.current.include_paths = config.include_paths.clone();
let current = cache.current.clone();
let sender = cache.sender.clone();
drop(cache);
if let Some(sender) = sender {
load_preview(sender, current, PostLoadBehavior::DoNothing);
}
}
};
}
/// If the file is in the cache, returns it.
/// In any was, register it as a dependency
fn get_file_from_cache(path: PathBuf) -> Option<String> {
@ -201,15 +225,11 @@ async fn reload_preview(
}
let mut builder = slint_interpreter::ComponentCompiler::default();
#[cfg(not(target_arch = "wasm32"))]
{
use clap::Parser;
let cli_args = super::Cli::parse();
if !cli_args.style.is_empty() {
builder.set_style(cli_args.style)
};
builder.set_include_paths(cli_args.include_paths);
if !preview_component.style.is_empty() {
builder.set_style(preview_component.style);
}
builder.set_include_paths(preview_component.include_paths);
builder.set_file_loader(|path| {
let path = path.to_owned();

View file

@ -178,7 +178,12 @@ pub fn handle_request(
.and_then(|token| {
#[cfg(feature = "preview")]
if token.0.kind() == SyntaxKind::Comment {
maybe_goto_preview(token.0, token.1, req.server_notifier());
maybe_goto_preview(
token.0,
token.1,
req.server_notifier(),
&document_cache.documents.compiler_config,
);
return None;
}
goto::goto_definition(document_cache, token.0)
@ -223,7 +228,11 @@ pub fn handle_request(
#[cfg(any(feature = "preview", feature = "preview-lense"))]
if params.command.as_str() == SHOW_PREVIEW_COMMAND {
#[cfg(feature = "preview")]
show_preview_command(&params.arguments, &req.server_notifier())?;
show_preview_command(
&params.arguments,
&req.server_notifier(),
&document_cache.documents.compiler_config,
)?;
return Ok(None::<serde_json::Value>);
}
if params.command.as_str() == QUERY_PROPERTIES_COMMAND {
@ -279,6 +288,7 @@ pub fn handle_request(
pub fn show_preview_command(
params: &[serde_json::Value],
connection: &crate::ServerNotifier,
config: &CompilerConfiguration,
) -> Result<(), Error> {
use crate::preview;
let e = || -> Error { "InvalidParameter".into() };
@ -291,7 +301,12 @@ pub fn show_preview_command(
let component = params.get(1).and_then(|v| v.as_str()).map(|v| v.to_string());
preview::load_preview(
connection.clone(),
preview::PreviewComponent { path: path_canon, component },
preview::PreviewComponent {
path: path_canon,
component,
include_paths: config.include_paths.clone(),
style: config.style.clone().unwrap_or_default(),
},
preview::PostLoadBehavior::ShowAfterLoad,
);
Ok(())
@ -337,6 +352,7 @@ fn maybe_goto_preview(
token: SyntaxToken,
offset: u32,
sender: crate::ServerNotifier,
compiler_config: &CompilerConfiguration,
) -> Option<()> {
use crate::preview;
let text = token.text();
@ -362,6 +378,8 @@ fn maybe_goto_preview(
preview::PreviewComponent {
path: token.source_file.path().into(),
component: Some(component_name),
include_paths: compiler_config.include_paths.clone(),
style: compiler_config.style.clone().unwrap_or_default(),
},
preview::PostLoadBehavior::ShowAfterLoad,
);
@ -687,6 +705,43 @@ fn get_code_lenses(
}
}
pub fn load_configuration(server_notifier: &crate::ServerNotifier) -> Result<(), Error> {
server_notifier.send_request::<lsp_types::request::WorkspaceConfiguration>(
lsp_types::ConfigurationParams {
items: vec![lsp_types::ConfigurationItem {
scope_uri: None,
section: Some("slint".into()),
}],
},
|result, document_cache| {
if let Ok(r) = result {
for v in r {
if let Some(o) = v.as_object() {
if let Some(ip) = o.get("includePath").and_then(|v| v.as_array()) {
if !ip.is_empty() {
document_cache.documents.compiler_config.include_paths = ip
.iter()
.filter_map(|x| x.as_str())
.map(std::path::PathBuf::from)
.collect();
}
}
if let Some(style) =
o.get("preview").and_then(|v| v.as_object()?.get("style")?.as_str())
{
if !style.is_empty() {
document_cache.documents.compiler_config.style = Some(style.into());
}
}
}
}
#[cfg(feature = "preview")]
crate::preview::config_changed(&document_cache.documents.compiler_config);
}
},
)
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -40,14 +40,42 @@ pub mod wasm_prelude {
}
#[derive(Clone)]
pub struct ServerNotifier(Function);
pub struct ServerNotifier {
send_notification: Function,
send_request: Function,
document_cache: Rc<RefCell<DocumentCache>>,
reentry_guard: Rc<RefCell<ReentryGuard>>,
}
impl ServerNotifier {
pub fn send_notification(&self, method: String, params: impl Serialize) -> Result<(), Error> {
self.0
self.send_notification
.call2(&JsValue::UNDEFINED, &method.into(), &to_value(&params)?)
.map_err(|x| format!("Error calling send_notification: {x:?}"))?;
Ok(())
}
pub fn send_request<T: lsp_types::request::Request>(
&self,
request: T::Params,
f: impl FnOnce(Result<T::Result, String>, &mut DocumentCache) + Send + 'static,
) -> Result<(), Error> {
let promise = self
.send_request
.call2(&JsValue::UNDEFINED, &T::METHOD.into(), &to_value(&request)?)
.map_err(|x| format!("Error calling send_request: {x:?}"))?;
let future = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise));
let document_cache = self.document_cache.clone();
let guard = self.reentry_guard.clone();
wasm_bindgen_futures::spawn_local(async move {
let r = future
.await
.map_err(|e| format!("{e:?}"))
.and_then(|v| serde_wasm_bindgen::from_value(v).map_err(|e| format!("{e:?}")));
let _lock = ReentryGuard::lock(guard).await;
f(r, &mut document_cache.borrow_mut());
});
Ok(())
}
}
#[derive(Debug, Clone)]
@ -130,12 +158,16 @@ impl Drop for ReentryGuardLock {
#[wasm_bindgen(typescript_custom_section)]
const IMPORT_CALLBACK_FUNCTION_SECTION: &'static str = r#"
type ImportCallbackFunction = (url: string) => Promise<string>;
type SendRequestFunction = (method: string, r: any) => Promise<any>;
"#;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "ImportCallbackFunction")]
pub type ImportCallbackFunction;
#[wasm_bindgen(typescript_type = "SendRequestFunction")]
pub type SendRequestFunction;
}
#[wasm_bindgen]
@ -150,6 +182,7 @@ pub struct SlintServer {
pub fn create(
init_param: JsValue,
send_notification: Function,
send_request: SendRequestFunction,
load_file: ImportCallbackFunction,
) -> Result<SlintServer, JsError> {
console_error_panic_hook::set_once();
@ -163,13 +196,20 @@ pub fn create(
Box::pin(async move { Some(self::load_file(path, &load_file).await) })
}));
let document_cache = DocumentCache::new(compiler_config);
let document_cache = Rc::new(RefCell::new(DocumentCache::new(compiler_config)));
let send_request = Function::from(send_request.clone());
let reentry_guard = Rc::new(RefCell::new(ReentryGuard::default()));
Ok(SlintServer {
document_cache: Rc::new(RefCell::new(document_cache)),
document_cache: document_cache.clone(),
init_param,
notifier: ServerNotifier(send_notification),
reentry_guard: Default::default(),
notifier: ServerNotifier {
send_notification,
send_request,
document_cache: document_cache.clone(),
reentry_guard: reentry_guard.clone(),
},
reentry_guard,
})
}
@ -236,6 +276,17 @@ impl SlintServer {
Ok(result.ok_or(JsError::new("Empty reply".into()))?)
})
}
#[wasm_bindgen]
pub fn reload_config(&self) -> Result<(), JsError> {
let guard = self.reentry_guard.clone();
let notifier = self.notifier.clone();
wasm_bindgen_futures::spawn_local(async move {
let _lock = ReentryGuard::lock(guard).await;
let _ = server_loop::load_configuration(&notifier);
});
Ok(())
}
}
async fn load_file(path: String, load_file: &Function) -> std::io::Result<String> {

View file

@ -23,12 +23,16 @@ slint_init().then((_) => {
return true;
}
async function send_request(method: string, params: any): Promise<any> {
return await connection.sendRequest(method, params);
}
async function load_file(path: string): Promise<string> {
return await connection.sendRequest("slint/load_file", path);
}
connection.onInitialize((params: InitializeParams): InitializeResult => {
the_lsp = slint_lsp.create(params, send_notification, load_file);
the_lsp = slint_lsp.create(params, send_notification, send_request, load_file);
const response = the_lsp.server_initialize_result();
response.capabilities.codeLensProvider = null; // CodeLenses are not relevant for the online editor
return response;