lsp: Notify preview about text edits

Have the LS notify the live preview about changes it initiates, so
the live preview can update its selection.

This is not possible for all edits the LS generates: Many are sent
to the editor which may or may not trigger them later, but the
notification happens when the LS adds changes on top of changes
requested by the live preview (e.g. by adding an import). This
fixes having a newly added element selected once it is rendered.
This commit is contained in:
Tobias Hunger 2024-02-15 14:53:54 +01:00 committed by Tobias Hunger
parent e4a0a85e2f
commit e1aefc6f16
4 changed files with 152 additions and 18 deletions

View file

@ -151,11 +151,29 @@ pub struct PreviewComponent {
#[allow(unused)]
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub enum LspToPreviewMessage {
SetContents { url: VersionedUrl, contents: String },
SetConfiguration { config: PreviewConfig },
SetContents {
url: VersionedUrl,
contents: String,
},
/// Adjust any selection in the document with `url` that is at or behind `offset` by `delta`
AdjustSelection {
url: VersionedUrl,
start_offset: u32,
end_offset: u32,
new_length: u32,
},
SetConfiguration {
config: PreviewConfig,
},
ShowPreview(PreviewComponent),
HighlightFromEditor { url: Option<Url>, offset: u32 },
KnownComponents { url: Option<VersionedUrl>, components: Vec<ComponentInformation> },
HighlightFromEditor {
url: Option<Url>,
offset: u32,
},
KnownComponents {
url: Option<VersionedUrl>,
components: Vec<ComponentInformation>,
},
}
#[allow(unused)]

View file

@ -23,7 +23,9 @@ use crate::util::{
#[cfg(target_arch = "wasm32")]
use crate::wasm_prelude::*;
use crate::ServerNotifier;
use i_slint_compiler::diagnostics::SourceFile;
use i_slint_compiler::object_tree::ElementRc;
use i_slint_compiler::parser::{syntax_nodes, NodeOrToken, SyntaxKind, SyntaxNode, SyntaxToken};
use i_slint_compiler::pathutils::clean_path;
@ -87,6 +89,31 @@ fn create_show_preview_command(
)
}
pub fn notify_preview_about_text_edit(
server_notifier: &ServerNotifier,
edit: &TextEdit,
source_file: &SourceFile,
) {
let new_length = edit.new_text.len() as u32;
let (start_offset, end_offset) = {
let so =
source_file.offset(edit.range.start.line as usize, edit.range.start.character as usize);
let eo =
source_file.offset(edit.range.end.line as usize, edit.range.end.character as usize);
(std::cmp::min(so, eo) as u32, std::cmp::max(so, eo) as u32)
};
let Ok(url) = Url::from_file_path(source_file.path()) else {
return;
};
server_notifier.send_message_to_preview(LspToPreviewMessage::AdjustSelection {
url: VersionedUrl::new(url, source_file.version()),
start_offset,
end_offset,
new_length,
});
}
#[cfg(feature = "preview-external")]
pub fn request_state(ctx: &std::rc::Rc<Context>) {
use i_slint_compiler::diagnostics::Spanned;
@ -406,7 +433,7 @@ pub fn register_request_handlers(rh: &mut RequestHandler) {
let p = tk.parent();
let version = p.source_file.version();
if let Some(value) = find_element_id_for_highlight(&tk, &tk.parent()) {
let edits = value
let edits: Vec<_> = value
.into_iter()
.map(|r| TextEdit {
range: map_range(&p.source_file, r),
@ -1374,6 +1401,9 @@ pub fn add_component(
if let Some(edit) =
completion::create_import_edit(doc, &component.component_type, &component.import_path)
{
if let Some(sf) = doc.node.as_ref().map(|n| &n.source_file) {
notify_preview_about_text_edit(&ctx.server_notifier, &edit, sf);
}
edits.push(edit);
}

View file

@ -2,10 +2,11 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
use crate::common::{
ComponentInformation, LspToPreviewMessage, PreviewComponent, PreviewConfig, UrlVersion,
VersionedUrl,
ComponentInformation, PreviewComponent, PreviewConfig, UrlVersion, VersionedUrl,
};
use crate::lsp_ext::Health;
use crate::preview::element_selection::ElementSelection;
use crate::util::map_position;
use i_slint_compiler::object_tree::ElementRc;
use i_slint_core::component_factory::FactoryContext;
use i_slint_core::model::VecModel;
@ -65,6 +66,7 @@ struct PreviewState {
ui: Option<ui::PreviewUi>,
handle: Rc<RefCell<Option<slint_interpreter::ComponentInstance>>>,
selected: Option<element_selection::ElementSelection>,
notify_editor_about_selection_after_update: bool,
}
thread_local! {static PREVIEW_STATE: std::cell::RefCell<PreviewState> = Default::default();}
@ -112,6 +114,7 @@ fn drop_component(
selection_offset,
is_layout,
None,
true,
);
send_message_to_lsp(crate::common::PreviewToLspMessage::AddComponent {
@ -172,6 +175,49 @@ pub fn config_changed(config: PreviewConfig) {
};
}
pub fn adjust_selection(url: VersionedUrl, start_offset: u32, end_offset: u32, new_length: u32) {
let Some((version, _)) = get_url_from_cache(url.url()) else {
return;
};
run_in_ui_thread(move || async move {
if &version != url.version() {
// We are outdated anyway, no use updating now.
return;
};
let Ok(path) = Url::to_file_path(url.url()) else {
return;
};
let Some(selected) = PREVIEW_STATE.with(move |preview_state| {
let preview_state = preview_state.borrow();
preview_state.selected.clone()
}) else {
return;
};
if selected.path != path {
// Not relevant for the current selection
return;
}
if selected.offset < start_offset {
// Nothing to do!
} else if selected.offset >= start_offset {
// Worst case if we get the offset wrong:
// Some other newarby element ends up getting marked as selected.
// So ignore special cases :-)
let old_length = end_offset - start_offset;
let offset = selected.offset + new_length - old_length;
PREVIEW_STATE.with(move |preview_state| {
let mut preview_state = preview_state.borrow_mut();
preview_state.selected = Some(ElementSelection { offset, ..selected });
});
}
})
}
/// If the file is in the cache, returns it.
/// In any way, register it as a dependency
fn get_url_from_cache(url: &Url) -> Option<(UrlVersion, String)> {
@ -206,9 +252,11 @@ pub fn load_preview(preview_component: PreviewComponent) {
};
run_in_ui_thread(move || async move {
let selected = PREVIEW_STATE.with(|preview_state| {
let (selected, notify_editor) = PREVIEW_STATE.with(|preview_state| {
let mut preview_state = preview_state.borrow_mut();
preview_state.selected.take()
let notify_editor = preview_state.notify_editor_about_selection_after_update;
preview_state.notify_editor_about_selection_after_update = false;
(preview_state.selected.take(), notify_editor)
});
loop {
@ -253,11 +301,36 @@ pub fn load_preview(preview_component: PreviewComponent) {
if let Some(se) = selected {
element_selection::select_element_at_source_code_position(
se.path,
se.path.clone(),
se.offset,
se.is_layout,
None,
false,
);
if notify_editor {
if let Some(component_instance) = component_instance() {
if let Some(element) = component_instance
.element_at_source_code_position(&se.path, se.offset)
.first()
{
if let Some(node) = element
.borrow()
.node
.iter()
.filter(|n| !crate::common::is_element_node_ignored(n))
.next()
{
let sf = &node.source_file;
let pos = map_position(sf, se.offset.into());
ask_editor_to_show_document(
&se.path.to_string_lossy(),
lsp_types::Range::new(pos.clone(), pos),
);
}
}
}
}
}
});
}
@ -365,7 +438,7 @@ pub fn highlight(url: Option<Url>, offset: u32) {
if let Some(e) = elements.first() {
let is_layout = e.borrow().layout.is_some();
element_selection::select_element_at_source_code_position(
path, offset, is_layout, None,
path, offset, is_layout, None, false,
);
} else {
element_selection::unselect_element();
@ -451,6 +524,7 @@ fn set_selections(
fn set_selected_element(
selection: Option<element_selection::ElementSelection>,
positions: slint_interpreter::highlight::ComponentPositions,
notify_editor_about_selection_after_update: bool,
) {
PREVIEW_STATE.with(move |preview_state| {
let mut preview_state = preview_state.borrow_mut();
@ -463,6 +537,8 @@ fn set_selected_element(
);
preview_state.selected = selection;
preview_state.notify_editor_about_selection_after_update =
notify_editor_about_selection_after_update;
})
}
@ -567,25 +643,29 @@ fn update_preview_area(compiled: Option<ComponentDefinition>) {
}
pub fn lsp_to_preview_message(
message: LspToPreviewMessage,
message: crate::common::LspToPreviewMessage,
#[cfg(not(target_arch = "wasm32"))] sender: &crate::ServerNotifier,
) {
use crate::common::LspToPreviewMessage as M;
match message {
LspToPreviewMessage::SetContents { url, contents } => {
M::SetContents { url, contents } => {
set_contents(&url, contents);
}
LspToPreviewMessage::SetConfiguration { config } => {
M::SetConfiguration { config } => {
config_changed(config);
}
LspToPreviewMessage::ShowPreview(pc) => {
M::AdjustSelection { url, start_offset, end_offset, new_length } => {
adjust_selection(url, start_offset, end_offset, new_length);
}
M::ShowPreview(pc) => {
#[cfg(not(target_arch = "wasm32"))]
native::open_ui(sender);
load_preview(pc);
}
LspToPreviewMessage::HighlightFromEditor { url, offset } => {
M::HighlightFromEditor { url, offset } => {
highlight(url, offset);
}
LspToPreviewMessage::KnownComponents { url, components } => {
M::KnownComponents { url, components } => {
known_components(&url, components);
}
}

View file

@ -11,6 +11,7 @@ use i_slint_core::lengths::{LogicalLength, LogicalPoint};
use rowan::TextRange;
use slint_interpreter::{highlight::ComponentPositions, ComponentInstance};
#[derive(Clone, Debug)]
pub struct ElementSelection {
pub path: PathBuf,
pub offset: u32,
@ -75,7 +76,7 @@ fn element_covers_point(
}
pub fn unselect_element() {
super::set_selected_element(None, ComponentPositions::default());
super::set_selected_element(None, ComponentPositions::default(), false);
}
pub fn select_element_at_source_code_position(
@ -83,6 +84,7 @@ pub fn select_element_at_source_code_position(
offset: u32,
is_layout: bool,
position: Option<LogicalPoint>,
notify_editor_about_selection_after_update: bool,
) {
let Some(component_instance) = super::component_instance() else {
return;
@ -93,6 +95,7 @@ pub fn select_element_at_source_code_position(
offset,
is_layout,
position,
notify_editor_about_selection_after_update,
)
}
@ -102,6 +105,7 @@ fn select_element_at_source_code_position_impl(
offset: u32,
is_layout: bool,
position: Option<LogicalPoint>,
notify_editor_about_selection_after_update: bool,
) {
let positions = component_instance.component_positions(&path, offset);
@ -114,6 +118,7 @@ fn select_element_at_source_code_position_impl(
super::set_selected_element(
Some(ElementSelection { path, offset, instance_index, is_layout }),
positions,
notify_editor_about_selection_after_update,
);
}
@ -129,6 +134,7 @@ fn select_element(
offset,
selected_element.borrow().layout.is_some(),
position,
false, // We update directly;-)
);
if let Some(document_position) = lsp_element_position(selected_element) {