Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: slint-lsp open to open the preview standalone #7576

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 71 additions & 40 deletions tools/lsp/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod language;
pub mod lsp_ext;
#[cfg(feature = "preview-engine")]
mod preview;
mod standalone;
pub mod util;

use common::Result;
Expand Down Expand Up @@ -82,11 +83,12 @@ pub struct Cli {
#[derive(Subcommand, Clone)]
enum Commands {
/// Format slint files
Format(Format),
Format(FormatCmd),
Open(OpenCmd),
}

#[derive(Args, Clone)]
struct Format {
struct FormatCmd {
#[arg(name = "path to .slint file(s)", action)]
paths: Vec<std::path::PathBuf>,

Expand All @@ -95,6 +97,54 @@ struct Format {
inline: bool,
}

#[derive(Args, Clone)]
struct OpenCmd {
#[arg(name = "path to .slint file", action)]
path: std::path::PathBuf,

/// The name of the component to view. If unset, the last exported component of the file is used.
/// If the component name is not in the .slint file , nothing will be shown
#[arg(long, value_name = "component name", action)]
component: Option<String>,
}

impl Cli {
fn into_compiler_config(
self,
send_message_to_preview: impl Fn(common::LspToPreviewMessage) + Clone + 'static,
) -> CompilerConfiguration {
CompilerConfiguration {
style: Some(if self.style.is_empty() { "native".into() } else { self.style }),
include_paths: self.include_paths,
library_paths: self
.library_paths
.iter()
.filter_map(|entry| {
entry.split('=').collect_tuple().map(|(k, v)| (k.into(), v.into()))
})
.collect(),
open_import_fallback: Some(Rc::new(move |path| {
let send_message_to_preview = send_message_to_preview.clone();
Box::pin(async move {
let contents = std::fs::read_to_string(&path);
if let Ok(url) = Url::from_file_path(&path) {
if let Ok(contents) = &contents {
send_message_to_preview(common::LspToPreviewMessage::SetContents {
url: common::VersionedUrl::new(url, None),
contents: contents.clone(),
})
} else {
send_message_to_preview(common::LspToPreviewMessage::ForgetFile { url })
}
}
Some(contents.map(|c| (None, c)))
})
})),
..Default::default()
}
}
}

enum OutgoingRequest {
Start,
Pending(Waker),
Expand Down Expand Up @@ -221,17 +271,27 @@ impl RequestHandler {
}

fn main() {
let args: Cli = Cli::parse();
let mut args: Cli = Cli::parse();
if !args.backend.is_empty() {
std::env::set_var("SLINT_BACKEND", &args.backend);
}

if let Some(Commands::Format(args)) = args.command {
let _ = fmt::tool::run(args.paths, args.inline).map_err(|e| {
eprintln!("{e}");
std::process::exit(1);
});
std::process::exit(0);
match args.command.take() {
Some(Commands::Format(args)) => {
let _ = fmt::tool::run(args.paths, args.inline).map_err(|e| {
eprintln!("{e}");
std::process::exit(1);
});
std::process::exit(0);
}
Some(Commands::Open(cmd)) => {
let _ = standalone::open(args, cmd.path, cmd.component).map_err(|e| {
eprintln!("{e}");
std::process::exit(1);
});
std::process::exit(0);
}
None => {}
}

if let Ok(panic_log_file) = std::env::var("SLINT_LSP_PANIC_LOG") {
Expand Down Expand Up @@ -334,37 +394,8 @@ fn main_loop(connection: Connection, init_param: InitializeParams, cli_args: Cli
preview::set_server_notifier(server_notifier.clone());

let server_notifier_ = server_notifier.clone();
let compiler_config = CompilerConfiguration {
style: Some(if cli_args.style.is_empty() { "native".into() } else { cli_args.style }),
include_paths: cli_args.include_paths,
library_paths: cli_args
.library_paths
.iter()
.filter_map(|entry| entry.split('=').collect_tuple().map(|(k, v)| (k.into(), v.into())))
.collect(),
open_import_fallback: Some(Rc::new(move |path| {
let server_notifier = server_notifier_.clone();
Box::pin(async move {
let contents = std::fs::read_to_string(&path);
if let Ok(url) = Url::from_file_path(&path) {
if let Ok(contents) = &contents {
server_notifier.send_message_to_preview(
common::LspToPreviewMessage::SetContents {
url: common::VersionedUrl::new(url, None),
contents: contents.clone(),
},
)
} else {
server_notifier.send_message_to_preview(
common::LspToPreviewMessage::ForgetFile { url },
)
}
}
Some(contents.map(|c| (None, c)))
})
})),
..Default::default()
};
let compiler_config =
cli_args.into_compiler_config(move |m| server_notifier_.send_message_to_preview(m));

let ctx = Rc::new(Context {
document_cache: RefCell::new(crate::common::DocumentCache::new(compiler_config)),
Expand Down
186 changes: 186 additions & 0 deletions tools/lsp/standalone.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright © SixtyFPS GmbH <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

use crate::OutgoingRequestQueue;
use core::cell::RefCell;
use core::future::Future;
use core::pin::Pin;
use core::task::Poll;
use lsp_types::notification::Notification;
use std::rc::Rc;
use std::sync::Arc;

pub fn open(
args: super::Cli,
path: std::path::PathBuf,
component: Option<String>,
) -> crate::common::Result<()> {
let cli_args = args.clone();
let lsp_thread = std::thread::Builder::new()
.name("LanguageServer".into())
.spawn(move || {
/// Make sure we quit the event loop even if we panic
struct QuitEventLoop;
impl Drop for QuitEventLoop {
fn drop(&mut self) {
super::preview::quit_ui_event_loop();
}
}
let quit_ui_loop = QuitEventLoop;
if let Err(e) = fake_lsp(args, path, component) {
eprintln!("{e}");
std::process::exit(1);
}
drop(quit_ui_loop);
})
.unwrap();

super::preview::start_ui_event_loop(cli_args);
lsp_thread.join().unwrap();
Ok(())
}

fn fake_lsp(
args: super::Cli,
path: std::path::PathBuf,
component: Option<String>,
) -> crate::common::Result<()> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a fake language server?

It feels easier to extend the live-preview to request the files it did not find than to do an entire extra parsing run to find those... And that is the only functionality we really need from the LS at this point in time.

let request_queue = OutgoingRequestQueue::default();
let (preview_to_lsp_sender, preview_to_lsp_receiver) =
crossbeam_channel::unbounded::<crate::common::PreviewToLspMessage>();
let (preview_to_client_sender, preview_to_client_reciever) =
crossbeam_channel::unbounded::<lsp_server::Message>();
let server_notifier = crate::ServerNotifier {
sender: preview_to_client_sender,
queue: request_queue.clone(),
use_external_preview: Default::default(),
preview_to_lsp_sender,
};

super::preview::set_server_notifier(server_notifier.clone());

let compiler_config = args.into_compiler_config(|m| super::preview::lsp_to_preview_message(m));

let init_param = lsp_types::InitializeParams {
capabilities: lsp_types::ClientCapabilities {
workspace: Some(lsp_types::WorkspaceClientCapabilities {
did_change_watched_files: Some(
lsp_types::DidChangeWatchedFilesClientCapabilities {
dynamic_registration: Some(true),
..Default::default()
},
),
..Default::default()
}),
..Default::default()
},
..Default::default()
};

let ctx = Rc::new(crate::Context {
document_cache: RefCell::new(crate::common::DocumentCache::new(compiler_config)),
preview_config: RefCell::new(Default::default()),
server_notifier,
init_param,
#[cfg(any(feature = "preview-external", feature = "preview-engine"))]
to_show: Default::default(),
open_urls: Default::default(),
});

let ctx_ = ctx.clone();
let contents = std::fs::read_to_string(&path)?;
let absolute_path = std::fs::canonicalize(path)?;
let future = Box::pin(async move {
let url = lsp_types::Url::from_file_path(&absolute_path).unwrap();
crate::open_document(
&ctx_,
contents,
url.clone(),
None,
&mut ctx_.document_cache.borrow_mut(),
)
.await?;
let mut args = vec![serde_json::to_value(url).unwrap()];
if let Some(component) = component {
args.push(serde_json::Value::String(component));
}
crate::language::show_preview_command(&args, &ctx_).map_err(|e| e.message)?;
Ok(())
});

// We are waiting in this loop for two kind of futures:
// - The compiler future should always be ready immediately because we do not set a callback to load files
// - the future from `send_request` are blocked waiting for a response from the client (us) and we make sure
// that they are available immediately.
struct DummyWaker;
impl std::task::Wake for DummyWaker {
fn wake(self: Arc<Self>) {}
}
let waker = Arc::new(DummyWaker).into();
let mut futures = Vec::<Pin<Box<dyn Future<Output = crate::common::Result<()>>>>>::new();
futures.push(future);

loop {
let mut result = Ok(());
futures.retain_mut(|f| {
if result.is_err() {
return true;
}
match f.as_mut().poll(&mut std::task::Context::from_waker(&waker)) {
Poll::Ready(x) => {
result = x;
false
}
Poll::Pending => true,
}
});
result?;
crossbeam_channel::select! {
recv(preview_to_client_reciever) -> msg => {
match msg? {
lsp_server::Message::Notification(n) if n.method == lsp_types::notification::PublishDiagnostics::METHOD => (),
msg => eprintln!("Got client message from preview: {msg:?}")
};
},
recv(preview_to_lsp_receiver) -> msg => {
use crate::common::PreviewToLspMessage as M;
match msg? {
M::Status { .. } => (),
M::Diagnostics { uri, version: _, diagnostics } => {
// print to stdout, what else can we do?
for d in diagnostics {
let severity = match d.severity {
Some(lsp_types::DiagnosticSeverity::ERROR) => "Error: ",
Some(lsp_types::DiagnosticSeverity::WARNING) => "Warning: ",
Some(lsp_types::DiagnosticSeverity::INFORMATION) => "Info: ",
Some(lsp_types::DiagnosticSeverity::HINT) => "Hint: ",
_ => "",
};
println!("{uri:?}:{} {severity}{}", d.range.start.line, d.message);
}
},
M::ShowDocument { .. } => (),
M::PreviewTypeChanged { .. } => unreachable!("can't change type to external"),
M::RequestState { unused: _ } => {
crate::language::request_state(&ctx);
},
M::SendWorkspaceEdit { label:_, edit } => {
let edits = crate::common::text_edit::apply_workspace_edit(&ctx.document_cache.borrow(), &edit)?;
for e in edits {
std::fs::write(e.url.to_file_path().unwrap(), &e.contents)?;
// FIXME: fs watcher should take care of this automatically
let ctx = ctx.clone();
futures.push(Box::pin(async move {
crate::language::reload_document(&ctx, e.contents, e.url, None, &mut ctx.document_cache.borrow_mut()).await
}));
}

},
M::SendShowMessage { message } => {
eprint!("{}", message.message);
},
};
},
};
}
}
Loading