mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-01 06:11:16 +00:00
360 lines
13 KiB
Rust
360 lines
13 KiB
Rust
/* LICENSE BEGIN
|
|
This file is part of the SixtyFPS Project -- https://sixtyfps.io
|
|
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
|
|
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
|
|
|
|
SPDX-License-Identifier: GPL-3.0-only
|
|
This file is also available under commercial licensing terms.
|
|
Please contact info@sixtyfps.io for more information.
|
|
LICENSE END */
|
|
|
|
mod preview;
|
|
|
|
use std::collections::HashMap;
|
|
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::{Completion, HoverRequest};
|
|
use lsp_types::{
|
|
CompletionOptions, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
|
|
GotoDefinitionResponse, Hover, HoverProviderCapability, InitializeParams, LocationLink,
|
|
MarkedString, OneOf, Position, PublishDiagnosticsParams, Range, ServerCapabilities,
|
|
TextDocumentSyncCapability, Url, WorkDoneProgressOptions,
|
|
};
|
|
use sixtyfps_compilerlib::diagnostics::{BuildDiagnostics, Spanned};
|
|
use sixtyfps_compilerlib::parser::{SyntaxKind, SyntaxNodeWithSourceFile};
|
|
use sixtyfps_compilerlib::typeloader::TypeLoader;
|
|
use sixtyfps_compilerlib::typeregister::TypeRegister;
|
|
use sixtyfps_compilerlib::CompilerConfiguration;
|
|
|
|
type Error = Box<dyn std::error::Error>;
|
|
|
|
struct DocumentCache<'a> {
|
|
documents: TypeLoader<'a>,
|
|
newline_offsets: HashMap<Url, Vec<u32>>,
|
|
}
|
|
|
|
impl<'a> DocumentCache<'a> {
|
|
fn new(config: &'a CompilerConfiguration) -> Self {
|
|
let documents =
|
|
TypeLoader::new(TypeRegister::builtin(), config, &mut BuildDiagnostics::default());
|
|
Self { documents, newline_offsets: Default::default() }
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
std::thread::spawn(|| {
|
|
match run_lsp_server() {
|
|
Ok(_) => {}
|
|
Err(error) => {
|
|
eprintln!("Error running LSP server: {}", error);
|
|
}
|
|
}
|
|
preview::quit_ui_event_loop();
|
|
});
|
|
// TODO: Don't terminate the event loop when the window is closed with Qt
|
|
// TODO: There's a race condition where theoretically the LSP could receive a preview
|
|
// request before the gui event loop has started, which would cause post_event to panic.
|
|
// Instead we should start the lsp thread when the gui thread is *ready*, for example through
|
|
// a single-shot timer.
|
|
preview::start_ui_event_loop();
|
|
}
|
|
|
|
fn run_lsp_server() -> Result<(), Error> {
|
|
let (connection, io_threads) = Connection::stdio();
|
|
let capabilities = ServerCapabilities {
|
|
completion_provider: Some(CompletionOptions {
|
|
resolve_provider: Some(true),
|
|
trigger_characters: None,
|
|
work_done_progress_options: WorkDoneProgressOptions::default(),
|
|
}),
|
|
hover_provider: Some(HoverProviderCapability::Simple(true)),
|
|
document_highlight_provider: Some(OneOf::Left(true)),
|
|
document_symbol_provider: Some(OneOf::Left(true)),
|
|
workspace_symbol_provider: Some(OneOf::Left(true)),
|
|
definition_provider: Some(OneOf::Left(true)),
|
|
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
|
lsp_types::TextDocumentSyncKind::Full,
|
|
)),
|
|
|
|
..ServerCapabilities::default()
|
|
};
|
|
let server_capabilities = serde_json::to_value(&capabilities).unwrap();
|
|
let initialization_params = connection.initialize(server_capabilities)?;
|
|
main_loop(&connection, initialization_params)?;
|
|
io_threads.join()?;
|
|
Ok(())
|
|
}
|
|
|
|
fn main_loop(connection: &Connection, params: serde_json::Value) -> Result<(), Error> {
|
|
let _params: InitializeParams = serde_json::from_value(params).unwrap();
|
|
let mut compiler_config = sixtyfps_compilerlib::CompilerConfiguration::new(
|
|
sixtyfps_compilerlib::generator::OutputFormat::Interpreter,
|
|
);
|
|
compiler_config.style = Some("ugly".into());
|
|
|
|
let mut document_cache = DocumentCache::new(&compiler_config);
|
|
for msg in &connection.receiver {
|
|
eprintln!("got msg: {:?}", msg);
|
|
match msg {
|
|
Message::Request(req) => {
|
|
if connection.handle_shutdown(&req)? {
|
|
return Ok(());
|
|
}
|
|
handle_request(connection, req, &mut document_cache)?;
|
|
}
|
|
Message::Response(_resp) => {}
|
|
Message::Notification(notifi) => {
|
|
handle_notification(connection, notifi, &mut document_cache)?
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_request(
|
|
connection: &Connection,
|
|
req: Request,
|
|
document_cache: &mut DocumentCache,
|
|
) -> Result<(), Error> {
|
|
let mut req = Some(req);
|
|
if let Some((id, params)) = cast::<GotoDefinition>(&mut req) {
|
|
let result = token_descr(document_cache, params.text_document_position_params)
|
|
.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::<Completion>(&mut req) {
|
|
/*let result = vec![
|
|
CompletionItem::new_simple("Hello".to_string(), "Some detail".to_string()),
|
|
CompletionItem::new_simple("Bye".to_string(), "More detail".to_string()),
|
|
];
|
|
let resp = Response::new_ok(id, result);
|
|
connection.sender.send(Message::Response(resp))?;*/
|
|
} else if let Some((id, params)) = cast::<HoverRequest>(&mut req) {
|
|
let result =
|
|
token_descr(document_cache, params.text_document_position_params).map(|x| Hover {
|
|
contents: lsp_types::HoverContents::Scalar(MarkedString::from_language_code(
|
|
"text".into(),
|
|
format!("{:?}", x.token),
|
|
)),
|
|
range: None,
|
|
});
|
|
let resp = Response::new_ok(id, result);
|
|
connection.sender.send(Message::Response(resp))?;
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
fn cast<Kind: lsp_types::request::Request>(
|
|
req: &mut Option<Request>,
|
|
) -> Option<(RequestId, Kind::Params)> {
|
|
match req.take().unwrap().extract::<Kind::Params>(Kind::METHOD) {
|
|
Ok(value) => Some(value),
|
|
Err(owned) => {
|
|
*req = Some(owned);
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
fn handle_notification(
|
|
connection: &Connection,
|
|
req: lsp_server::Notification,
|
|
document_cache: &mut DocumentCache,
|
|
) -> Result<(), Error> {
|
|
match &*req.method {
|
|
DidOpenTextDocument::METHOD => {
|
|
let params: DidOpenTextDocumentParams = serde_json::from_value(req.params)?;
|
|
reload_document(
|
|
connection,
|
|
params.text_document.text,
|
|
params.text_document.uri,
|
|
document_cache,
|
|
)?;
|
|
}
|
|
DidChangeTextDocument::METHOD => {
|
|
let mut params: DidChangeTextDocumentParams = serde_json::from_value(req.params)?;
|
|
reload_document(
|
|
connection,
|
|
params.content_changes.pop().unwrap().text,
|
|
params.text_document.uri,
|
|
document_cache,
|
|
)?;
|
|
}
|
|
"sixtyfps/showPreview" => {
|
|
let e = || -> Error { "InvalidParameter".into() };
|
|
let path = if let serde_json::Value::String(s) = req.params.get(0).ok_or_else(e)? {
|
|
s
|
|
} else {
|
|
return Err(e());
|
|
};
|
|
preview::load_preview(path.into());
|
|
}
|
|
_ => (),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn newline_offsets_from_content(content: &str) -> Vec<u32> {
|
|
let mut ln_offs = 0;
|
|
content
|
|
.split('\n')
|
|
.map(|line| {
|
|
let r = ln_offs;
|
|
ln_offs += line.len() as u32 + 1;
|
|
r
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn reload_document(
|
|
connection: &Connection,
|
|
content: String,
|
|
uri: lsp_types::Url,
|
|
document_cache: &mut DocumentCache,
|
|
) -> Result<(), Error> {
|
|
let newline_offsets = newline_offsets_from_content(&content);
|
|
document_cache.newline_offsets.insert(uri.clone(), newline_offsets);
|
|
|
|
let path = uri.to_file_path().unwrap();
|
|
let path_canon = path.canonicalize().unwrap_or_else(|_| path.to_owned());
|
|
let mut diag = BuildDiagnostics::default();
|
|
spin_on::spin_on(document_cache.documents.load_file(&path_canon, &path, content, &mut diag));
|
|
|
|
// Always provide diagnostics for all files. Empty diagnostics clear any previous ones.
|
|
let mut lsp_diags: HashMap<Url, Vec<lsp_types::Diagnostic>> = core::iter::once(&path)
|
|
.chain(diag.all_loaded_files.iter())
|
|
.map(|path| {
|
|
let uri = Url::from_file_path(path).unwrap();
|
|
(uri, Default::default())
|
|
})
|
|
.collect();
|
|
|
|
for d in diag.into_iter() {
|
|
if d.source_file().unwrap().is_relative() {
|
|
continue;
|
|
}
|
|
let uri = Url::from_file_path(d.source_file().unwrap()).unwrap();
|
|
lsp_diags.entry(uri).or_default().push(to_lsp_diag(&d));
|
|
}
|
|
|
|
for (uri, diagnostics) in lsp_diags {
|
|
connection.sender.send(Message::Notification(lsp_server::Notification::new(
|
|
"textDocument/publishDiagnostics".into(),
|
|
PublishDiagnosticsParams { uri, diagnostics, version: None },
|
|
)))?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn to_lsp_diag_level(
|
|
level: sixtyfps_compilerlib::diagnostics::DiagnosticLevel,
|
|
) -> lsp_types::DiagnosticSeverity {
|
|
match level {
|
|
sixtyfps_interpreter::DiagnosticLevel::Error => lsp_types::DiagnosticSeverity::Error,
|
|
sixtyfps_interpreter::DiagnosticLevel::Warning => lsp_types::DiagnosticSeverity::Warning,
|
|
}
|
|
}
|
|
|
|
fn to_lsp_diag(d: &sixtyfps_compilerlib::diagnostics::Diagnostic) -> lsp_types::Diagnostic {
|
|
lsp_types::Diagnostic::new(
|
|
to_range(d.line_column()),
|
|
Some(to_lsp_diag_level(d.level())),
|
|
None,
|
|
None,
|
|
d.message().to_owned(),
|
|
None,
|
|
None,
|
|
)
|
|
}
|
|
|
|
fn to_range(span: (usize, usize)) -> Range {
|
|
let pos = Position::new((span.0 as u32).saturating_sub(1), (span.1 as u32).saturating_sub(1));
|
|
Range::new(pos, pos)
|
|
}
|
|
|
|
fn token_descr(
|
|
document_cache: &DocumentCache,
|
|
lsp_position: lsp_types::TextDocumentPositionParams,
|
|
) -> Option<sixtyfps_compilerlib::parser::SyntaxTokenWithSourceFile> {
|
|
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 doc =
|
|
document_cache.documents.get_document(Path::new(lsp_position.text_document.uri.path()))?;
|
|
let node = doc.node.as_ref()?;
|
|
let token = node.0.node.token_at_offset(o.into()).last()?;
|
|
Some(sixtyfps_compilerlib::parser::SyntaxTokenWithSourceFile {
|
|
token,
|
|
source_file: node.0.source_file.clone(),
|
|
})
|
|
//Some(format!("{:?}", token))
|
|
}
|
|
|
|
fn goto_definition(
|
|
document_cache: &mut DocumentCache,
|
|
token: sixtyfps_compilerlib::parser::SyntaxNodeWithSourceFile,
|
|
) -> Option<GotoDefinitionResponse> {
|
|
match token.kind() {
|
|
SyntaxKind::QualifiedName => {
|
|
let source_file = token.source_file.clone()?;
|
|
let parent = token.node.parent()?;
|
|
let qual =
|
|
sixtyfps_compilerlib::object_tree::QualifiedTypeName::from_node(token.into());
|
|
match parent.kind() {
|
|
SyntaxKind::Element => {
|
|
let doc = document_cache.documents.get_document(source_file.path())?;
|
|
match doc.local_registry.lookup_qualified(&qual.members) {
|
|
sixtyfps_compilerlib::langtype::Type::Component(c) => {
|
|
goto_node(document_cache, &c.root_element.borrow().node.as_ref()?.0)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn goto_node(
|
|
document_cache: &mut DocumentCache,
|
|
node: &SyntaxNodeWithSourceFile,
|
|
) -> Option<GotoDefinitionResponse> {
|
|
let path = node.source_file.as_ref()?.path();
|
|
let target_uri = Url::from_file_path(path).ok()?;
|
|
let newline_offsets = match document_cache.newline_offsets.entry(target_uri.clone()) {
|
|
std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
|
|
std::collections::hash_map::Entry::Vacant(e) => {
|
|
e.insert(newline_offsets_from_content(&std::fs::read_to_string(path).ok()?))
|
|
}
|
|
};
|
|
let offset = node.span().offset as u32;
|
|
let pos = newline_offsets.binary_search(&offset).map_or_else(
|
|
|line| {
|
|
if line == 0 {
|
|
Position::new(0, offset)
|
|
} else {
|
|
Position::new(
|
|
line as u32 - 1,
|
|
newline_offsets.get(line - 1).map_or(0, |x| offset - *x),
|
|
)
|
|
}
|
|
},
|
|
|line| Position::new(line as u32, 0),
|
|
);
|
|
let range = Range::new(pos, pos);
|
|
Some(GotoDefinitionResponse::Link(vec![LocationLink {
|
|
origin_selection_range: None,
|
|
target_uri,
|
|
target_range: range,
|
|
target_selection_range: range,
|
|
}]))
|
|
}
|