slint/tools/lsp/server_loop.rs
Tobias Hunger a9b09f21f1 LSP: Add toggleDesignMode command
Add a toggle command to the LS and use them in VSCode.

Keep the existing set_design_mode commands for Slintpad, as it is easier
for the state stored on the web site and in the WASM side to go out of
sync. This is not an issue in VS Code at this point.
2023-04-25 14:49:54 +02:00

1688 lines
61 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
// cSpell: ignore descr rfind
#[cfg(target_arch = "wasm32")]
use crate::wasm_prelude::*;
use crate::{completion, goto, semantic_tokens, util};
use i_slint_compiler::diagnostics::{BuildDiagnostics, Spanned};
use i_slint_compiler::langtype::Type;
use i_slint_compiler::object_tree::ElementRc;
use i_slint_compiler::parser::{syntax_nodes, NodeOrToken, SyntaxKind, SyntaxNode, SyntaxToken};
use i_slint_compiler::typeloader::TypeLoader;
use i_slint_compiler::typeregister::TypeRegister;
use i_slint_compiler::CompilerConfiguration;
use lsp_types::request::{
CodeActionRequest, CodeLensRequest, ColorPresentationRequest, Completion, DocumentColor,
DocumentHighlightRequest, DocumentSymbolRequest, ExecuteCommand, GotoDefinition, HoverRequest,
PrepareRenameRequest, Rename, SemanticTokensFullRequest,
};
use lsp_types::{
ClientCapabilities, CodeActionOrCommand, CodeActionProviderCapability, CodeLens,
CodeLensOptions, Color, ColorInformation, ColorPresentation, Command, CompletionOptions,
DocumentSymbol, DocumentSymbolResponse, Hover, InitializeParams, InitializeResult, OneOf,
Position, PrepareRenameResponse, PublishDiagnosticsParams, RenameOptions,
SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, ServerCapabilities,
ServerInfo, TextDocumentSyncCapability, TextEdit, Url, WorkDoneProgressOptions, WorkspaceEdit,
};
use std::cell::RefCell;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;
pub type Error = Box<dyn std::error::Error>;
/// Trim leading and trailing whitespace tokens from the `range`
/// `token` must be the first token in that `range`.
/// This should actually not be necessary (the parser should not
/// add stray whitespace tokens), but it makes working with the LSP so
/// much nicer that I want this here till we fix the parser.
fn range_from_token(token: Option<SyntaxToken>, range: &rowan::TextRange) -> rowan::TextRange {
let mut current_token = token;
let mut start_pos = None;
let mut end_pos = None;
while let Some(ct) = current_token {
current_token = ct.next_token();
if ct.text_range().end() > range.end() {
break;
}
if ct.kind() != SyntaxKind::Whitespace {
let r = ct.text_range();
start_pos = Some(r.start());
end_pos = Some(r.end());
break;
}
}
while let Some(ct) = current_token {
current_token = ct.next_token();
if ct.text_range().end() > range.end() {
break;
}
if ct.kind() != SyntaxKind::Whitespace {
end_pos = Some(ct.text_range().end());
}
}
rowan::TextRange::new(start_pos.unwrap_or(range.start()), end_pos.unwrap_or(range.end()))
}
pub struct ProgressReporter {
token: Option<lsp_types::ProgressToken>,
notifier: crate::ServerNotifier,
}
impl ProgressReporter {
// Use ServerNotifier::progress_reporter!
pub fn new(
notifier: crate::ServerNotifier,
token: lsp_types::ProgressToken,
title: String,
message: Option<String>,
percentage: Option<u32>,
cancellable: Option<bool>,
) -> Result<Self, Error> {
notifier.send_notification(
"$/progress".to_string(),
lsp_types::ProgressParams {
token: token.clone(),
value: lsp_types::ProgressParamsValue::WorkDone(
lsp_types::WorkDoneProgress::Begin(lsp_types::WorkDoneProgressBegin {
title,
cancellable,
message,
percentage,
}),
),
},
)?;
Ok(Self { notifier, token: Some(token) })
}
pub fn update(
&self,
message: Option<String>,
percentage: Option<u32>,
cancellable: Option<bool>,
) -> Result<(), Error> {
if let Some(token) = &self.token {
self.notifier.send_notification(
"$/progress".to_string(),
lsp_types::ProgressParams {
token: token.clone(),
value: lsp_types::ProgressParamsValue::WorkDone(
lsp_types::WorkDoneProgress::Report(lsp_types::WorkDoneProgressReport {
cancellable,
message,
percentage,
}),
),
},
)
} else {
Err("Progress reporting was finished already".into())
}
}
pub fn finish(mut self, message: Option<String>) -> Result<(), Error> {
self.finish_impl(message)
}
fn finish_impl(&mut self, message: Option<String>) -> Result<(), Error> {
let token = self.token.take();
if let Some(token) = token {
self.notifier.send_notification(
"$/progress".to_string(),
lsp_types::ProgressParams {
token,
value: lsp_types::ProgressParamsValue::WorkDone(
lsp_types::WorkDoneProgress::End(lsp_types::WorkDoneProgressEnd {
message,
}),
),
},
)
} else {
Err("Progress reporting was finished already".into())
}
}
}
impl Drop for ProgressReporter {
fn drop(&mut self) {
let _ = self.finish_impl(None);
}
}
const QUERY_PROPERTIES_COMMAND: &str = "slint/queryProperties";
const REMOVE_BINDING_COMMAND: &str = "slint/removeBinding";
const SHOW_PREVIEW_COMMAND: &str = "slint/showPreview";
const SET_BINDING_COMMAND: &str = "slint/setBinding";
const SET_DESIGN_MODE_COMMAND: &str = "slint/setDesignMode";
const TOGGLE_DESIGN_MODE_COMMAND: &str = "slint/toggleDesignMode";
fn command_list() -> Vec<String> {
vec![
QUERY_PROPERTIES_COMMAND.into(),
REMOVE_BINDING_COMMAND.into(),
#[cfg(any(feature = "preview", feature = "preview-lense"))]
SHOW_PREVIEW_COMMAND.into(),
#[cfg(any(feature = "preview", feature = "preview-lense"))]
SET_DESIGN_MODE_COMMAND.into(),
SET_BINDING_COMMAND.into(),
#[cfg(any(feature = "preview", feature = "preview-lense"))]
TOGGLE_DESIGN_MODE_COMMAND.into(),
]
}
fn create_show_preview_command(pretty: bool, file: &str, component_name: &str) -> Command {
let title = format!("{}Show Preview", if pretty { &"" } else { &"" });
Command::new(title, SHOW_PREVIEW_COMMAND.into(), Some(vec![file.into(), component_name.into()]))
}
pub struct DocumentCache {
pub(crate) documents: TypeLoader,
newline_offsets: HashMap<Url, Rc<Vec<u32>>>,
versions: HashMap<Url, i32>,
}
impl DocumentCache {
pub fn new(config: CompilerConfiguration) -> Self {
let documents =
TypeLoader::new(TypeRegister::builtin(), config, &mut BuildDiagnostics::default());
Self { documents, newline_offsets: Default::default(), versions: Default::default() }
}
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()
}
pub fn document_version(&self, target_uri: &lsp_types::Url) -> Option<i32> {
self.versions.get(target_uri).cloned()
}
fn newline_offsets_of_url(&mut self, uri: &lsp_types::Url) -> Option<Rc<Vec<u32>>> {
let newline_offsets = match self.newline_offsets.entry(uri.clone()) {
std::collections::hash_map::Entry::Occupied(e) => e.into_mut(),
std::collections::hash_map::Entry::Vacant(e) => {
let path = uri.to_file_path().ok()?;
let content =
self.documents.get_document(&path)?.node.as_ref()?.source_file()?.source()?;
e.insert(Rc::new(Self::newline_offsets_from_content(content)))
}
};
Some(newline_offsets.clone())
}
pub fn offset_to_position_mapper(
&mut self,
uri: &Url,
) -> Result<OffsetToPositionMapper, Error> {
self.newline_offsets_of_url(uri)
.map(OffsetToPositionMapper)
.ok_or_else(|| Into::<Error>::into("Document not found in cache"))
}
}
pub struct OffsetToPositionMapper(Rc<Vec<u32>>);
impl OffsetToPositionMapper {
pub fn map_u32(&self, offset: u32) -> lsp_types::Position {
self.0.binary_search(&offset).map_or_else(
|line| {
if line == 0 {
Position::new(0, offset)
} else {
Position::new(line as u32 - 1, self.0.get(line - 1).map_or(0, |x| offset - *x))
}
},
|line| Position::new(line as u32, 0),
)
}
pub fn map(&self, s: rowan::TextSize) -> lsp_types::Position {
self.map_u32(s.into())
}
pub fn map_range(&self, r: rowan::TextRange) -> lsp_types::Range {
lsp_types::Range::new(self.map(r.start()), self.map(r.end()))
}
/// This strips leading/trailing whitespace tokens from the node
pub fn map_node(&self, n: &SyntaxNode) -> lsp_types::Range {
self.map_range(range_from_token(n.first_token(), &n.text_range()))
}
}
#[cfg(feature = "preview-api")]
pub struct PreviewApi {
pub highlight: Box<
dyn Fn(
&Rc<Context>,
Option<std::path::PathBuf>,
u32,
) -> Result<(), Box<dyn std::error::Error>>,
>,
}
pub struct Context {
pub document_cache: RefCell<DocumentCache>,
pub server_notifier: crate::ServerNotifier,
pub init_param: InitializeParams,
#[cfg(feature = "preview-api")]
pub preview: PreviewApi,
}
#[derive(Default)]
pub struct RequestHandler(
pub HashMap<
&'static str,
Box<
dyn Fn(
serde_json::Value,
Rc<Context>,
) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, Error>>>>,
>,
>,
);
impl RequestHandler {
pub fn register<
R: lsp_types::request::Request,
Fut: Future<Output = Result<R::Result, Error>> + 'static,
>(
&mut self,
handler: fn(R::Params, Rc<Context>) -> Fut,
) where
R::Params: 'static,
{
self.0.insert(
R::METHOD,
Box::new(move |value, ctx| {
Box::pin(async move {
let params = serde_json::from_value(value)
.map_err(|e| format!("error when deserializing request: {e:?}"))?;
handler(params, ctx).await.map(|x| serde_json::to_value(x).unwrap())
})
}),
);
}
}
pub fn server_initialize_result(client_cap: &ClientCapabilities) -> InitializeResult {
InitializeResult {
capabilities: ServerCapabilities {
completion_provider: Some(CompletionOptions {
resolve_provider: None,
trigger_characters: Some(vec![".".to_owned()]),
work_done_progress_options: WorkDoneProgressOptions::default(),
all_commit_characters: None,
completion_item: None,
}),
definition_provider: Some(OneOf::Left(true)),
text_document_sync: Some(TextDocumentSyncCapability::Kind(
lsp_types::TextDocumentSyncKind::FULL,
)),
code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
execute_command_provider: Some(lsp_types::ExecuteCommandOptions {
commands: command_list(),
..Default::default()
}),
document_symbol_provider: Some(OneOf::Left(true)),
color_provider: Some(true.into()),
code_lens_provider: Some(CodeLensOptions { resolve_provider: Some(true) }),
semantic_tokens_provider: Some(
SemanticTokensOptions {
legend: SemanticTokensLegend {
token_types: semantic_tokens::LEGEND_TYPES.to_vec(),
token_modifiers: semantic_tokens::LEGEND_MODS.to_vec(),
},
full: Some(SemanticTokensFullOptions::Bool(true)),
..Default::default()
}
.into(),
),
document_highlight_provider: Some(OneOf::Left(true)),
rename_provider: Some(
if client_cap
.text_document
.as_ref()
.and_then(|td| td.rename.as_ref())
.and_then(|r| r.prepare_support)
.unwrap_or(false)
{
OneOf::Right(RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: WorkDoneProgressOptions::default(),
})
} else {
OneOf::Left(true)
},
),
..ServerCapabilities::default()
},
server_info: Some(ServerInfo {
name: env!("CARGO_PKG_NAME").to_string(),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
}),
offset_encoding: Some("utf-8".to_string()),
}
}
pub fn register_request_handlers(rh: &mut RequestHandler) {
rh.register::<GotoDefinition, _>(|params, ctx| async move {
let document_cache = &mut ctx.document_cache.borrow_mut();
let result = token_descr(
document_cache,
&params.text_document_position_params.text_document.uri,
&params.text_document_position_params.position,
)
.and_then(|token| {
#[cfg(feature = "preview")]
if token.0.kind() == SyntaxKind::Comment {
maybe_goto_preview(
token.0,
token.1,
ctx.server_notifier.clone(),
&document_cache.documents.compiler_config,
);
return None;
}
goto::goto_definition(document_cache, token.0)
});
Ok(result)
});
rh.register::<Completion, _>(|params, ctx| async move {
let document_cache = &mut ctx.document_cache.borrow_mut();
let result = token_descr(
document_cache,
&params.text_document_position.text_document.uri,
&params.text_document_position.position,
)
.and_then(|token| {
completion::completion_at(
document_cache,
token.0,
token.1,
ctx.init_param
.capabilities
.text_document
.as_ref()
.and_then(|t| t.completion.as_ref()),
)
});
Ok(result)
});
rh.register::<HoverRequest, _>(|_params, _ctx| async move {
/*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(None::<Hover>)
});
rh.register::<CodeActionRequest, _>(|params, ctx| async move {
let document_cache = &mut ctx.document_cache.borrow_mut();
let result = token_descr(document_cache, &params.text_document.uri, &params.range.start)
.and_then(|token| get_code_actions(document_cache, token.0.parent()));
Ok(result)
});
rh.register::<ExecuteCommand, _>(|params, ctx| async move {
if params.command.as_str() == SHOW_PREVIEW_COMMAND {
#[cfg(feature = "preview")]
show_preview_command(&params.arguments, &ctx)?;
return Ok(None::<serde_json::Value>);
}
if params.command.as_str() == SET_DESIGN_MODE_COMMAND {
#[cfg(feature = "preview")]
set_design_mode(&params.arguments, &ctx)?;
return Ok(None::<serde_json::Value>);
}
if params.command.as_str() == TOGGLE_DESIGN_MODE_COMMAND {
#[cfg(feature = "preview")]
toggle_design_mode(&params.arguments, &ctx)?;
return Ok(None::<serde_json::Value>);
}
if params.command.as_str() == QUERY_PROPERTIES_COMMAND {
return Ok(Some(query_properties_command(&params.arguments, &ctx)?));
}
if params.command.as_str() == SET_BINDING_COMMAND {
return Ok(Some(set_binding_command(&params.arguments, &ctx).await?));
}
if params.command.as_str() == REMOVE_BINDING_COMMAND {
return Ok(Some(remove_binding_command(&params.arguments, &ctx).await?));
}
Ok(None::<serde_json::Value>)
});
rh.register::<DocumentColor, _>(|params, ctx| async move {
let document_cache = &mut ctx.document_cache.borrow_mut();
Ok(get_document_color(document_cache, &params.text_document).unwrap_or_default())
});
rh.register::<ColorPresentationRequest, _>(|params, _ctx| async move {
// Convert the color from the color picker to a string representation. This could try to produce a minimal
// representation.
let requested_color = params.color;
let color_literal = if requested_color.alpha < 1. {
format!(
"#{:0>2x}{:0>2x}{:0>2x}{:0>2x}",
(requested_color.red * 255.) as u8,
(requested_color.green * 255.) as u8,
(requested_color.blue * 255.) as u8,
(requested_color.alpha * 255.) as u8
)
} else {
format!(
"#{:0>2x}{:0>2x}{:0>2x}",
(requested_color.red * 255.) as u8,
(requested_color.green * 255.) as u8,
(requested_color.blue * 255.) as u8,
)
};
Ok(vec![ColorPresentation { label: color_literal, ..Default::default() }])
});
rh.register::<DocumentSymbolRequest, _>(|params, ctx| async move {
let document_cache = &mut ctx.document_cache.borrow_mut();
Ok(get_document_symbols(document_cache, &params.text_document))
});
rh.register::<CodeLensRequest, _>(|params, ctx| async move {
let document_cache = &mut ctx.document_cache.borrow_mut();
Ok(get_code_lenses(document_cache, &params.text_document))
});
rh.register::<SemanticTokensFullRequest, _>(|params, ctx| async move {
let document_cache = &mut ctx.document_cache.borrow_mut();
Ok(semantic_tokens::get_semantic_tokens(document_cache, &params.text_document))
});
rh.register::<DocumentHighlightRequest, _>(|_params, ctx| async move {
let document_cache = &mut ctx.document_cache.borrow_mut();
let uri = _params.text_document_position_params.text_document.uri;
if let Some((tk, _off)) =
token_descr(document_cache, &uri, &_params.text_document_position_params.position)
{
let offset_mapper = document_cache.offset_to_position_mapper(&uri)?;
let p = tk.parent();
#[cfg(feature = "preview-api")]
if p.kind() == SyntaxKind::QualifiedName
&& p.parent().map_or(false, |n| n.kind() == SyntaxKind::Element)
{
(ctx.preview.highlight)(&ctx, uri.to_file_path().ok(), _off)?;
let range = offset_mapper.map_range(p.text_range());
return Ok(Some(vec![lsp_types::DocumentHighlight { range, kind: None }]));
} else {
(ctx.preview.highlight)(&ctx, None, 0)?;
}
if let Some(value) = find_element_id_for_highlight(&tk, &p) {
return Ok(Some(
value
.into_iter()
.map(|r| lsp_types::DocumentHighlight {
range: offset_mapper.map_range(r),
kind: None,
})
.collect(),
));
}
}
#[cfg(feature = "preview-api")]
(ctx.preview.highlight)(&ctx, None, 0)?;
Ok(None)
});
rh.register::<Rename, _>(|params, ctx| async move {
let mut document_cache = ctx.document_cache.borrow_mut();
let uri = params.text_document_position.text_document.uri;
if let Some((tk, _off)) =
token_descr(&mut document_cache, &uri, &params.text_document_position.position)
{
if let Some(value) = find_element_id_for_highlight(&tk, &tk.parent()) {
let mapper = document_cache.offset_to_position_mapper(&uri)?;
let edits = value
.into_iter()
.map(|r| TextEdit {
range: mapper.map_range(r),
new_text: params.new_name.clone(),
})
.collect();
return Ok(Some(WorkspaceEdit {
changes: Some(std::iter::once((uri, edits)).collect()),
..Default::default()
}));
}
};
Err("This symbol cannot be renamed. (Only element id can be renamed at the moment)".into())
});
rh.register::<PrepareRenameRequest, _>(|params, ctx| async move {
let mut document_cache = ctx.document_cache.borrow_mut();
let uri = params.text_document.uri;
if let Some((tk, _off)) = token_descr(&mut document_cache, &uri, &params.position) {
if find_element_id_for_highlight(&tk, &tk.parent()).is_some() {
return Ok(Some(PrepareRenameResponse::Range(
document_cache.offset_to_position_mapper(&uri)?.map_range(tk.text_range()),
)));
}
};
Ok(None)
});
}
#[cfg(feature = "preview")]
pub fn show_preview_command(params: &[serde_json::Value], ctx: &Rc<Context>) -> Result<(), Error> {
let document_cache = &mut ctx.document_cache.borrow_mut();
let config = &document_cache.documents.compiler_config;
let connection = &ctx.server_notifier;
use crate::preview;
let e = || "InvalidParameter";
let url = if let serde_json::Value::String(s) = params.get(0).ok_or_else(e)? {
Url::parse(&s)?
} else {
return Err(e().into());
};
let component = params.get(1).and_then(|v| v.as_str()).map(|v| v.to_string());
let path = url.to_file_path().unwrap_or_default();
let path_canon = dunce::canonicalize(&path).unwrap_or(path);
preview::load_preview(
connection.clone(),
preview::PreviewComponent {
path: path_canon,
component,
include_paths: config.include_paths.clone(),
style: config.style.clone().unwrap_or_default(),
},
preview::PostLoadBehavior::ShowAfterLoad,
);
Ok(())
}
#[cfg(feature = "preview")]
pub fn set_design_mode(params: &[serde_json::Value], ctx: &Rc<Context>) -> Result<(), Error> {
let connection = &ctx.server_notifier;
use crate::preview;
let e = || "InvalidParameter";
let enable = if let serde_json::Value::Bool(b) = params.get(0).ok_or_else(e)? {
b
} else {
return Err(e().into());
};
preview::set_design_mode(connection.clone(), *enable);
Ok(())
}
#[cfg(feature = "preview")]
pub fn toggle_design_mode(_params: &[serde_json::Value], ctx: &Rc<Context>) -> Result<(), Error> {
let connection = &ctx.server_notifier;
use crate::preview;
preview::set_design_mode(connection.clone(), !preview::design_mode());
Ok(())
}
pub fn query_properties_command(
params: &[serde_json::Value],
ctx: &Rc<Context>,
) -> Result<serde_json::Value, Error> {
let document_cache = &mut ctx.document_cache.borrow_mut();
use crate::properties;
let text_document_uri = serde_json::from_value::<lsp_types::TextDocumentIdentifier>(
params.get(0).ok_or("No text document provided")?.clone(),
)?
.uri;
let position = serde_json::from_value::<lsp_types::Position>(
params.get(1).ok_or("No position provided")?.clone(),
)?;
let source_version = if let Some(v) = document_cache.document_version(&text_document_uri) {
v
} else {
return Ok(serde_json::to_value(properties::QueryPropertyResponse::no_element_response(
text_document_uri.to_string(),
-1,
))
.expect("Failed to serialize none-element property query result!"));
};
if let Some(element) = element_at_position(document_cache, &text_document_uri, &position) {
properties::query_properties(document_cache, &text_document_uri, source_version, &element)
.map(|r| serde_json::to_value(r).expect("Failed to serialize property query result!"))
} else {
Ok(serde_json::to_value(properties::QueryPropertyResponse::no_element_response(
text_document_uri.to_string(),
source_version,
))
.expect("Failed to serialize none-element property query result!"))
}
}
pub async fn set_binding_command(
params: &[serde_json::Value],
ctx: &Rc<Context>,
) -> Result<serde_json::Value, Error> {
use crate::properties;
let text_document = serde_json::from_value::<lsp_types::OptionalVersionedTextDocumentIdentifier>(
params.get(0).ok_or("No text document provided")?.clone(),
)?;
let element_range = serde_json::from_value::<lsp_types::Range>(
params.get(1).ok_or("No element range provided")?.clone(),
)?;
let property_name = serde_json::from_value::<String>(
params.get(2).ok_or("No property name provided")?.clone(),
)?;
let new_expression =
serde_json::from_value::<String>(params.get(3).ok_or("No expression provided")?.clone())?;
let dry_run = {
if let Some(p) = params.get(4) {
serde_json::from_value::<bool>(p.clone())
} else {
Ok(true)
}
}?;
let (result, edit) = {
let document_cache = &mut ctx.document_cache.borrow_mut();
let uri = text_document.uri;
if let Some(source_version) = text_document.version {
if let Some(current_version) = document_cache.document_version(&uri) {
if current_version != source_version {
return Err(
"Document version mismatch. Please refresh your property information"
.into(),
);
}
} else {
return Err(format!("Document with uri {uri} not found in cache").into());
}
}
let element =
element_at_position(document_cache, &uri, &element_range.start).ok_or_else(|| {
format!("No element found at the given start position {:?}", &element_range.start)
})?;
let offset_mapper = document_cache.offset_to_position_mapper(&uri)?;
let node_range = offset_mapper.map_node(
element
.borrow()
.node
.as_ref()
.ok_or("The element was found, but had no range defined!")?,
);
if node_range.start != element_range.start {
return Err(format!(
"Element found, but does not start at the expected place (){:?} != {:?}).",
node_range.start, element_range.start
)
.into());
}
if node_range.end != element_range.end {
return Err(format!(
"Element found, but does not end at the expected place (){:?} != {:?}).",
node_range.end, element_range.end
)
.into());
}
properties::set_binding(document_cache, &uri, &element, &property_name, new_expression)?
};
if !dry_run {
if let Some(edit) = edit {
let response = ctx
.server_notifier
.send_request::<lsp_types::request::ApplyWorkspaceEdit>(
lsp_types::ApplyWorkspaceEditParams { label: Some("set binding".into()), edit },
)?
.await?;
if !response.applied {
return Err(response
.failure_reason
.unwrap_or("Operation failed, no specific reason given".into())
.into());
}
}
}
Ok(serde_json::to_value(result).expect("Failed to serialize set_binding result!"))
}
pub async fn remove_binding_command(
params: &[serde_json::Value],
ctx: &Rc<Context>,
) -> Result<serde_json::Value, Error> {
use crate::properties;
let text_document = serde_json::from_value::<lsp_types::OptionalVersionedTextDocumentIdentifier>(
params.get(0).ok_or("No text document provided")?.clone(),
)?;
let element_range = serde_json::from_value::<lsp_types::Range>(
params.get(1).ok_or("No element range provided")?.clone(),
)?;
let property_name = serde_json::from_value::<String>(
params.get(2).ok_or("No property name provided")?.clone(),
)?;
let edit = {
let document_cache = &mut ctx.document_cache.borrow_mut();
let uri = text_document.uri;
let offset_mapper = document_cache.offset_to_position_mapper(&uri)?;
if let Some(source_version) = text_document.version {
if let Some(current_version) = document_cache.document_version(&uri) {
if current_version != source_version {
return Err(
"Document version mismatch. Please refresh your property information"
.into(),
);
}
} else {
return Err(format!("Document with uri {uri} not found in cache").into());
}
}
let element =
element_at_position(document_cache, &uri, &element_range.start).ok_or_else(|| {
format!("No element found at the given start position {:?}", &element_range.start)
})?;
let node_range = offset_mapper.map_node(
element
.borrow()
.node
.as_ref()
.ok_or("The element was found, but had no range defined!")?,
);
if node_range.start != element_range.start {
return Err(format!(
"Element found, but does not start at the expected place (){:?} != {:?}).",
node_range.start, element_range.start
)
.into());
}
if node_range.end != element_range.end {
return Err(format!(
"Element found, but does not end at the expected place (){:?} != {:?}).",
node_range.end, element_range.end
)
.into());
}
properties::remove_binding(document_cache, &uri, &element, &property_name)?
};
let response = ctx
.server_notifier
.send_request::<lsp_types::request::ApplyWorkspaceEdit>(
lsp_types::ApplyWorkspaceEditParams { label: Some("set binding".into()), edit },
)?
.await?;
if !response.applied {
return Err(response
.failure_reason
.unwrap_or("Operation failed, no specific reason given".into())
.into());
}
Ok(serde_json::to_value(()).expect("Failed to serialize ()!"))
}
#[cfg(feature = "preview")]
/// Workaround for editor that do not support code action: using the goto definition on a comment
/// that says "preview" will show the preview.
fn maybe_goto_preview(
token: SyntaxToken,
offset: u32,
sender: crate::ServerNotifier,
compiler_config: &CompilerConfiguration,
) -> Option<()> {
use crate::preview;
let text = token.text();
let offset = offset.checked_sub(token.text_range().start().into())? as usize;
if offset > text.len() || offset == 0 {
return None;
}
let begin = text[..offset].rfind(|x: char| !x.is_ascii_alphanumeric())? + 1;
let text = &text.as_bytes()[begin..];
let rest = text.strip_prefix(b"preview").or_else(|| text.strip_prefix(b"PREVIEW"))?;
if rest.first().map_or(true, |x| x.is_ascii_alphanumeric()) {
return None;
}
// Ok, we were hovering on PREVIEW
let mut node = token.parent();
loop {
if let Some(component) = syntax_nodes::Component::new(node.clone()) {
let component_name =
i_slint_compiler::parser::identifier_text(&component.DeclaredIdentifier())?;
preview::load_preview(
sender,
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,
);
return Some(());
}
node = node.parent()?;
}
}
pub async fn reload_document_impl(
mut content: String,
uri: lsp_types::Url,
version: i32,
document_cache: &mut DocumentCache,
) -> Result<HashMap<Url, Vec<lsp_types::Diagnostic>>, Error> {
let path = uri.to_file_path().unwrap();
if path.extension().map_or(false, |e| e == "rs") {
content = match extract_rust_macro(content) {
Some(content) => content,
// A rust file without a rust macro, just ignore it
None => return Ok([(uri, vec![])].into_iter().collect()),
};
}
let newline_offsets = Rc::new(DocumentCache::newline_offsets_from_content(&content));
document_cache.newline_offsets.insert(uri.clone(), newline_offsets);
document_cache.versions.insert(uri.clone(), version);
let path_canon = dunce::canonicalize(&path).unwrap_or_else(|_| path.to_owned());
#[cfg(feature = "preview")]
crate::preview::set_contents(&path_canon, content.clone());
let mut diag = BuildDiagnostics::default();
document_cache.documents.load_file(&path_canon, &path, content, false, &mut diag).await;
// 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() {
#[cfg(not(target_arch = "wasm32"))]
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(util::to_lsp_diag(&d));
}
Ok(lsp_diags)
}
pub async fn reload_document(
connection: &crate::ServerNotifier,
content: String,
uri: lsp_types::Url,
version: i32,
document_cache: &mut DocumentCache,
) -> Result<(), Error> {
let lsp_diags = reload_document_impl(content, uri, version, document_cache).await?;
for (uri, diagnostics) in lsp_diags {
connection.send_notification(
"textDocument/publishDiagnostics".into(),
PublishDiagnosticsParams { uri, diagnostics, version: None },
)?;
}
Ok(())
}
fn extract_rust_macro(content: String) -> Option<String> {
let mut begin = 0;
let (open, close) = loop {
if let Some(m) = content[begin..].find("slint") {
// heuristics to find if we are not in a comment or a string literal. Not perfect, but should work in most cases
if let Some(x) = content[begin..(begin + m)].rfind(['\\', '\n', '/', '\"']) {
if content.as_bytes()[begin + x] != b'\n' {
begin += m + 5;
begin += content[begin..].find(['\n']).unwrap_or(0);
continue;
}
}
begin += m + 5;
while content[begin..].starts_with(' ') {
begin += 1;
}
if !content[begin..].starts_with('!') {
continue;
}
begin += 1;
while content[begin..].starts_with(' ') {
begin += 1;
}
let Some(open) = content.as_bytes().get(begin) else { continue };
match open {
b'{' => break (SyntaxKind::LBrace, SyntaxKind::RBrace),
b'[' => break (SyntaxKind::LBracket, SyntaxKind::RBracket),
b'(' => break (SyntaxKind::LParent, SyntaxKind::RParent),
_ => continue,
}
} else {
// No macro found, just return
return None;
}
};
begin += 1;
// Now find the matching closing delimiter
// Technically, we should be lexing rust, not slint
let mut state = i_slint_compiler::lexer::LexState::default();
let mut end = begin;
let mut level = 0;
while !content[end..].is_empty() {
let len = match i_slint_compiler::parser::lex_next_token(&content[end..], &mut state) {
Some((len, x)) if x == open => {
level += 1;
len
}
Some((_, x)) if x == close && level == 0 => {
break;
}
Some((len, x)) if x == close => {
level -= 1;
len
}
Some((len, _)) => len,
None => {
// Lex error
break;
}
};
if len == 0 {
break; // Shouldn't happen
}
end += len;
}
let mut bytes = content.into_bytes();
for c in &mut bytes[..begin] {
if *c != b'\n' {
*c = b' '
}
}
for c in &mut bytes[end..] {
if *c != b'\n' {
*c = b' '
}
}
Some(String::from_utf8(bytes).expect("We just added spaces"))
}
#[test]
fn test_extract_rust_macro() {
assert_eq!(extract_rust_macro("\nslint{!{}}".into()), None);
assert_eq!(
extract_rust_macro(
"abc\n\nslint ! {x \" \\\" }🦀\" { () {}\n {} }xx =}- ;}\n xxx \n yyy {}\n".into(),
),
Some(
" \n \n x \" \\\" }🦀\" { () {}\n {} }xx = \n \n \n".into(),
)
);
assert_eq!(
extract_rust_macro("xx\nabcd::slint!{abc{}efg".into()),
Some(" \n abc{}efg".into())
);
assert_eq!(
extract_rust_macro("slint!\nnot.\nslint!{\nunterminated\nxxx".into()),
Some(" \n \n \nunterminated\nxxx".into())
);
assert_eq!(extract_rust_macro("foo\n/* slint! { hello }\n".into()), None);
assert_eq!(extract_rust_macro("foo\n/* slint::slint! { hello }\n".into()), None);
assert_eq!(
extract_rust_macro("foo\n// slint! { hello }\nslint!{world}\na".into()),
Some(" \n \n world \n ".into())
);
assert_eq!(extract_rust_macro("foo\n\" slint! { hello }\"\n".into()), None);
assert_eq!(
extract_rust_macro(
"abc\n\nslint ! (x /* \\\" )🦀*/ { () {}\n {} }xx =)- ;}\n xxx \n yyy {}\n".into(),
),
Some(
" \n \n x /* \\\" )🦀*/ { () {}\n {} }xx = \n \n \n".into(),
)
);
assert_eq!(
extract_rust_macro("abc slint![x slint!() [{[]}] s] abc".into()),
Some(" x slint!() [{[]}] s ".into()),
);
}
fn get_document_and_offset<'a>(
document_cache: &'a mut DocumentCache,
text_document_uri: &'a Url,
pos: &'a Position,
) -> Option<(&'a i_slint_compiler::object_tree::Document, u32)> {
let o = document_cache.newline_offsets.get(text_document_uri)?.get(pos.line as usize)?
+ pos.character;
let doc = document_cache.documents.get_document(&text_document_uri.to_file_path().ok()?)?;
doc.node.as_ref()?.text_range().contains_inclusive(o.into()).then_some((doc, o))
}
fn element_contains(element: &i_slint_compiler::object_tree::ElementRc, offset: u32) -> bool {
element.borrow().node.as_ref().map_or(false, |n| n.text_range().contains(offset.into()))
}
pub fn element_at_position(
document_cache: &mut DocumentCache,
text_document_uri: &Url,
pos: &Position,
) -> Option<i_slint_compiler::object_tree::ElementRc> {
let (doc, offset) = get_document_and_offset(document_cache, text_document_uri, pos)?;
for component in &doc.inner_components {
let mut element = component.root_element.clone();
while element_contains(&element, offset) {
if let Some(c) =
element.clone().borrow().children.iter().find(|c| element_contains(c, offset))
{
element = c.clone();
} else {
return Some(element);
}
}
}
None
}
/// return the token, and the offset within the file
fn token_descr(
document_cache: &mut DocumentCache,
text_document_uri: &Url,
pos: &Position,
) -> Option<(SyntaxToken, u32)> {
let (doc, o) = get_document_and_offset(document_cache, text_document_uri, pos)?;
let node = doc.node.as_ref()?;
let mut taf = node.token_at_offset(o.into());
let token = match (taf.next(), taf.next()) {
(None, _) => node.last_token()?,
(Some(t), None) => t,
(Some(l), Some(r)) => match (l.kind(), r.kind()) {
// Prioritize identifier
(SyntaxKind::Identifier, _) => l,
(_, SyntaxKind::Identifier) => r,
// then the dot
(SyntaxKind::Dot, _) => l,
(_, SyntaxKind::Dot) => r,
// de-prioritize the white spaces
(SyntaxKind::Whitespace, _) => r,
(SyntaxKind::Comment, _) => r,
(_, SyntaxKind::Whitespace) => l,
(_, SyntaxKind::Comment) => l,
_ => l,
},
};
Some((SyntaxToken { token, source_file: node.source_file.clone() }, o))
}
fn get_code_actions(
_document_cache: &mut DocumentCache,
node: SyntaxNode,
) -> Option<Vec<CodeActionOrCommand>> {
if cfg!(feature = "preview-lense") {
let component = syntax_nodes::Component::new(node.clone())
.or_else(|| {
syntax_nodes::DeclaredIdentifier::new(node.clone())
.and_then(|n| n.parent())
.and_then(syntax_nodes::Component::new)
})
.or_else(|| {
syntax_nodes::QualifiedName::new(node.clone())
.and_then(|n| n.parent())
.and_then(syntax_nodes::Element::new)
.and_then(|n| n.parent())
.and_then(syntax_nodes::Component::new)
})?;
let component_name =
i_slint_compiler::parser::identifier_text(&component.DeclaredIdentifier())?;
Some(vec![CodeActionOrCommand::Command(create_show_preview_command(
false,
&component.source_file.path().to_string_lossy(),
&component_name,
))])
} else {
None
}
}
fn get_document_color(
document_cache: &mut DocumentCache,
text_document: &lsp_types::TextDocumentIdentifier,
) -> Option<Vec<ColorInformation>> {
let mut result = Vec::new();
let uri = &text_document.uri;
let offset_mapper = document_cache.offset_to_position_mapper(uri).ok()?;
let doc = document_cache.documents.get_document(&uri.to_file_path().ok()?)?;
let root_node = &doc.node.as_ref()?.node;
let mut token = root_node.first_token()?;
loop {
if token.kind() == SyntaxKind::ColorLiteral {
(|| -> Option<()> {
let range = offset_mapper.map_range(token.text_range());
let col = i_slint_compiler::literals::parse_color_literal(token.text())?;
let shift = |s: u32| -> f32 { ((col >> s) & 0xff) as f32 / 255. };
result.push(ColorInformation {
range,
color: Color {
alpha: shift(24),
red: shift(16),
green: shift(8),
blue: shift(0),
},
});
Some(())
})();
}
token = match token.next_token() {
Some(token) => token,
None => break Some(result),
}
}
}
fn get_document_symbols(
document_cache: &mut DocumentCache,
text_document: &lsp_types::TextDocumentIdentifier,
) -> Option<DocumentSymbolResponse> {
let uri = &text_document.uri;
let offset_mapper = document_cache.offset_to_position_mapper(uri).ok()?;
let doc = document_cache.documents.get_document(&uri.to_file_path().ok()?)?;
// DocumentSymbol doesn't implement default and some field depends on features or are deprecated
let ds: DocumentSymbol = serde_json::from_value(
serde_json::json!({ "name" : "", "kind": 255, "range" : lsp_types::Range::default(), "selectionRange": lsp_types::Range::default() })
)
.unwrap();
let inner_components = doc.inner_components.clone();
let inner_structs = doc.inner_structs.clone();
let mut r = inner_components
.iter()
.filter_map(|c| {
let root_element = c.root_element.borrow();
let element_node = root_element.node.as_ref()?;
let component_node = syntax_nodes::Component::new(element_node.parent()?)?;
let selection_range = offset_mapper.map_node(&component_node.DeclaredIdentifier());
Some(DocumentSymbol {
range: offset_mapper.map_node(&component_node),
selection_range,
name: c.id.clone(),
kind: if c.is_global() {
lsp_types::SymbolKind::OBJECT
} else {
lsp_types::SymbolKind::CLASS
},
children: gen_children(&c.root_element, &ds, &offset_mapper),
..ds.clone()
})
})
.collect::<Vec<_>>();
r.extend(inner_structs.iter().filter_map(|c| match c {
Type::Struct { name: Some(name), node: Some(node), .. } => Some(DocumentSymbol {
range: offset_mapper.map_node(node.parent().as_ref()?),
selection_range: offset_mapper.map_node(node),
name: name.clone(),
kind: lsp_types::SymbolKind::STRUCT,
..ds.clone()
}),
_ => None,
}));
fn gen_children(
elem: &ElementRc,
ds: &DocumentSymbol,
offset_mapper: &OffsetToPositionMapper,
) -> Option<Vec<DocumentSymbol>> {
let r = elem
.borrow()
.children
.iter()
.filter_map(|child| {
let e = child.borrow();
Some(DocumentSymbol {
range: offset_mapper.map_node(e.node.as_ref()?),
selection_range: offset_mapper
.map_node(e.node.as_ref()?.QualifiedName().as_ref()?),
name: e.base_type.to_string(),
detail: (!e.id.is_empty()).then(|| e.id.clone()),
kind: lsp_types::SymbolKind::VARIABLE,
children: gen_children(child, ds, offset_mapper),
..ds.clone()
})
})
.collect::<Vec<_>>();
(!r.is_empty()).then_some(r)
}
r.sort_by(|a, b| {
if a.range.start.line.cmp(&b.range.start.line) == std::cmp::Ordering::Less {
std::cmp::Ordering::Less
} else if a.range.start.line.cmp(&b.range.start.line) == std::cmp::Ordering::Equal {
a.range.start.character.cmp(&b.range.start.character)
} else {
std::cmp::Ordering::Greater
}
});
Some(r.into())
}
fn get_code_lenses(
document_cache: &mut DocumentCache,
text_document: &lsp_types::TextDocumentIdentifier,
) -> Option<Vec<CodeLens>> {
if cfg!(feature = "preview-lense") {
let uri = &text_document.uri;
let offset_mapper = document_cache.offset_to_position_mapper(uri).ok()?;
let filepath = uri.to_file_path().ok()?;
let doc = document_cache.documents.get_document(&filepath)?;
let inner_components = doc.inner_components.clone();
let mut r = vec![];
// Handle preview lens
r.extend(inner_components.iter().filter(|c| !c.is_global()).filter_map(|c| {
Some(CodeLens {
range: offset_mapper.map_range(c.root_element.borrow().node.as_ref()?.text_range()),
command: Some(create_show_preview_command(true, uri.as_str(), c.id.as_str())),
data: None,
})
}));
Some(r)
} else {
None
}
}
/// If the token is matching a Element ID, return the list of all element id in the same component
fn find_element_id_for_highlight(
token: &SyntaxToken,
parent: &SyntaxNode,
) -> Option<Vec<rowan::TextRange>> {
fn is_element_id(tk: &SyntaxToken, parent: &SyntaxNode) -> bool {
if tk.kind() != SyntaxKind::Identifier {
return false;
}
if parent.kind() == SyntaxKind::SubElement {
return true;
};
if parent.kind() == SyntaxKind::QualifiedName
&& matches!(
parent.parent().map(|n| n.kind()),
Some(SyntaxKind::Expression | SyntaxKind::StatePropertyChange)
)
{
let mut c = parent.children_with_tokens();
if let Some(NodeOrToken::Token(first)) = c.next() {
return first.text_range() == tk.text_range()
&& matches!(c.next(), Some(NodeOrToken::Token(second)) if second.kind() == SyntaxKind::Dot);
}
}
false
}
if is_element_id(token, parent) {
// An id: search all use of the id in this Component
let mut candidate = parent.parent();
while let Some(c) = candidate {
if c.kind() == SyntaxKind::Component {
let mut ranges = Vec::new();
let mut found_definition = false;
recurse(&mut ranges, &mut found_definition, c, token.text());
fn recurse(
ranges: &mut Vec<rowan::TextRange>,
found_definition: &mut bool,
c: SyntaxNode,
text: &str,
) {
for x in c.children_with_tokens() {
match x {
NodeOrToken::Node(n) => recurse(ranges, found_definition, n, text),
NodeOrToken::Token(tk) => {
if is_element_id(&tk, &c) && tk.text() == text {
ranges.push(tk.text_range());
if c.kind() == SyntaxKind::SubElement {
*found_definition = true;
}
}
}
}
}
}
if !found_definition {
return None;
}
return Some(ranges);
}
candidate = c.parent()
}
}
None
}
pub async fn load_configuration(ctx: &Context) -> Result<(), Error> {
if !ctx
.init_param
.capabilities
.workspace
.as_ref()
.and_then(|w| w.configuration)
.unwrap_or(false)
{
return Ok(());
}
let r = ctx
.server_notifier
.send_request::<lsp_types::request::WorkspaceConfiguration>(
lsp_types::ConfigurationParams {
items: vec![lsp_types::ConfigurationItem {
scope_uri: None,
section: Some("slint".into()),
}],
},
)?
.await?;
let document_cache = &mut ctx.document_cache.borrow_mut();
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());
}
}
}
}
// Always load the widgets so we can auto-complete them
let mut diag = BuildDiagnostics::default();
document_cache.documents.import_component("std-widgets.slint", "StyleMetrics", &mut diag).await;
#[cfg(feature = "preview")]
crate::preview::config_changed(&document_cache.documents.compiler_config);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::{complex_document_cache, loaded_document_cache};
#[test]
fn test_reload_document_invalid_contents() {
let (_, url, diag) = loaded_document_cache("fluent", "This is not valid!".into());
assert!(diag.len() == 1); // Only one URL is known
let diagnostics = diag.get(&url).expect("URL not found in result");
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].severity, Some(lsp_types::DiagnosticSeverity::ERROR));
}
#[test]
fn test_reload_document_text_positions() {
let (mut dc, url, _) = loaded_document_cache(
"fluent",
// cspell:disable-next-line
"Thiß is not valid!\n and more...".into(),
);
let mapper = dc.offset_to_position_mapper(&url).unwrap();
assert_eq!(mapper.map_u32(0), lsp_types::Position { line: 0, character: 0 });
assert_eq!(mapper.map_u32(4), lsp_types::Position { line: 0, character: 4 });
assert_eq!(mapper.map_u32(5), lsp_types::Position { line: 0, character: 5 }); // TODO: Figure out whether this is actually correct...
assert_eq!(mapper.map_u32(1024), lsp_types::Position { line: 1, character: 1004 });
// TODO: This is nonsense!
}
#[test]
fn test_reload_document_valid_contents() {
let (_, url, diag) = loaded_document_cache(
"fluent",
r#"export component Main inherits Rectangle { }"#.into(),
);
assert!(diag.len() == 1); // Only one URL is known
let diagnostics = diag.get(&url).expect("URL not found in result");
assert!(diagnostics.is_empty());
}
#[test]
fn test_text_document_color_no_color_set() {
let (mut dc, url, _) = loaded_document_cache(
"fluent",
r#"
component Main inherits Rectangle { }
"#
.into(),
);
let result =
get_document_color(&mut dc, &lsp_types::TextDocumentIdentifier { uri: url.clone() })
.expect("Color Vec was returned");
assert!(result.is_empty());
}
#[test]
fn test_text_document_color_rgba_color() {
let (mut dc, url, _) = loaded_document_cache(
"fluent",
r#"
component Main inherits Rectangle {
background: #1200FF80;
}
"#
.into(),
);
let result =
get_document_color(&mut dc, &lsp_types::TextDocumentIdentifier { uri: url.clone() })
.expect("Color Vec was returned");
assert_eq!(result.len(), 1);
let start = &result[0].range.start;
assert_eq!(start.line, 2);
assert_eq!(start.character, 28); // TODO: Why is this not 30?
let end = &result[0].range.end;
assert_eq!(end.line, 2);
assert_eq!(end.character, 37); // TODO: Why is this not 39?
let color = &result[0].color;
assert_eq!(f64::trunc(color.red as f64 * 255.0), 18.0);
assert_eq!(f64::trunc(color.green as f64 * 255.0), 0.0);
assert_eq!(f64::trunc(color.blue as f64 * 255.0), 255.0);
assert_eq!(f64::trunc(color.alpha as f64 * 255.0), 128.0);
}
fn id_at_position(
dc: &mut DocumentCache,
url: &Url,
line: u32,
character: u32,
) -> Option<String> {
let result = element_at_position(dc, &url, &Position { line, character })?;
let element = result.borrow();
Some(element.id.clone())
}
fn base_type_at_position(
dc: &mut DocumentCache,
url: &Url,
line: u32,
character: u32,
) -> Option<String> {
let result = element_at_position(dc, &url, &Position { line, character })?;
let element = result.borrow();
Some(format!("{}", &element.base_type))
}
#[test]
fn test_element_at_position_no_element() {
let (mut dc, url, _) = complex_document_cache("fluent");
assert_eq!(id_at_position(&mut dc, &url, 0, 10), None);
// TODO: This is past the end of the line and should thus return None
assert_eq!(id_at_position(&mut dc, &url, 42, 90), Some(String::new()));
assert_eq!(id_at_position(&mut dc, &url, 1, 0), None);
assert_eq!(id_at_position(&mut dc, &url, 55, 1), None);
assert_eq!(id_at_position(&mut dc, &url, 56, 5), None);
}
#[test]
fn test_element_at_position_no_such_document() {
let (mut dc, _, _) = complex_document_cache("fluent");
assert_eq!(
id_at_position(&mut dc, &Url::parse("https://foo.bar/baz").unwrap(), 5, 0),
None
);
}
#[test]
fn test_element_at_position_root() {
let (mut dc, url, _) = complex_document_cache("fluent");
assert_eq!(id_at_position(&mut dc, &url, 2, 29), Some("root".to_string())); // TODO: Seems one char too early..
assert_eq!(id_at_position(&mut dc, &url, 2, 32), Some("root".to_string()));
assert_eq!(id_at_position(&mut dc, &url, 2, 42), Some("root".to_string()));
assert_eq!(id_at_position(&mut dc, &url, 3, 0), Some("root".to_string()));
assert_eq!(id_at_position(&mut dc, &url, 3, 53), Some("root".to_string()));
assert_eq!(id_at_position(&mut dc, &url, 4, 19), Some("root".to_string()));
assert_eq!(id_at_position(&mut dc, &url, 5, 0), Some("root".to_string()));
assert_eq!(id_at_position(&mut dc, &url, 6, 8), Some("root".to_string()));
assert_eq!(id_at_position(&mut dc, &url, 6, 15), Some("root".to_string()));
assert_eq!(id_at_position(&mut dc, &url, 6, 23), Some("root".to_string()));
assert_eq!(id_at_position(&mut dc, &url, 8, 15), Some("root".to_string()));
assert_eq!(id_at_position(&mut dc, &url, 12, 3), Some("root".to_string())); // right before child // TODO: Seems wrong!
assert_eq!(id_at_position(&mut dc, &url, 51, 5), Some("root".to_string())); // right after child // TODO: Why does this not work?
assert_eq!(id_at_position(&mut dc, &url, 52, 0), Some("root".to_string()));
}
#[test]
fn test_element_at_position_child() {
let (mut dc, url, _) = complex_document_cache("fluent");
assert_eq!(base_type_at_position(&mut dc, &url, 12, 4), Some("VerticalBox".to_string()));
assert_eq!(base_type_at_position(&mut dc, &url, 14, 22), Some("HorizontalBox".to_string()));
assert_eq!(base_type_at_position(&mut dc, &url, 15, 33), Some("Text".to_string()));
assert_eq!(base_type_at_position(&mut dc, &url, 27, 4), Some("VerticalBox".to_string()));
assert_eq!(base_type_at_position(&mut dc, &url, 28, 8), Some("Text".to_string()));
assert_eq!(base_type_at_position(&mut dc, &url, 51, 4), Some("VerticalBox".to_string()));
}
#[test]
fn test_document_symbols() {
let (mut dc, uri, _) = complex_document_cache("fluent");
let result =
get_document_symbols(&mut dc, &lsp_types::TextDocumentIdentifier { uri }).unwrap();
if let DocumentSymbolResponse::Nested(result) = result {
assert_eq!(result.len(), 1);
let first = result.get(0).unwrap();
assert_eq!(&first.name, "MainWindow");
} else {
unreachable!();
}
}
#[test]
fn test_document_symbols_hello_world() {
let (mut dc, uri, _) = loaded_document_cache(
"fluent",
r#"import { Button, VerticalBox } from "std-widgets.slint";
component Demo {
VerticalBox {
alignment: start;
Text {
text: "Hello World!";
font-size: 24px;
horizontal-alignment: center;
}
Image {
source: @image-url("https://slint-ui.com/logo/slint-logo-full-light.svg");
height: 100px;
}
HorizontalLayout { alignment: center; Button { text: "OK!"; } }
}
}
"#
.into(),
);
let result =
get_document_symbols(&mut dc, &lsp_types::TextDocumentIdentifier { uri }).unwrap();
if let DocumentSymbolResponse::Nested(result) = result {
assert_eq!(result.len(), 1);
let first = result.get(0).unwrap();
assert_eq!(&first.name, "Demo");
} else {
unreachable!();
}
}
}