Preserve cursor position for modal insertions

This commit is contained in:
2026-08-01 19:50:00 +02:00
parent 932b73cd97
commit a74165fae5
4 changed files with 90 additions and 10 deletions

View File

@@ -6897,14 +6897,15 @@ impl BdsApp {
}
fn insert_link_modal(&mut self, post_id: &str) -> Task<Message> {
let state = match self.post_editors.get(post_id) {
Some(s) => s.clone(),
let (title, insertion_point) = match self.post_editors.get(post_id) {
Some(state) => (state.title.clone(), state.insertion_point()),
None => return Task::none(),
};
self.active_modal = Some(modal::ModalState::PostInsertLink {
post_id: post_id.to_string(),
title: state.title,
title,
insertion_point,
results: self.query_post_link_results(post_id, ""),
search_query: String::new(),
active_tab: modal::PostInsertLinkTab::Internal,
@@ -6915,14 +6916,15 @@ impl BdsApp {
}
fn insert_media_modal(&mut self, post_id: &str, link_only: bool) -> Task<Message> {
let state = match self.post_editors.get(post_id) {
Some(s) => s.clone(),
let (title, insertion_point) = match self.post_editors.get(post_id) {
Some(state) => (state.title.clone(), state.insertion_point()),
None => return Task::none(),
};
self.active_modal = Some(modal::ModalState::InsertMedia {
post_id: post_id.to_string(),
title: state.title,
title,
insertion_point,
media_list: self.query_post_insert_media_results(""),
search_query: String::new(),
link_only,
@@ -7076,10 +7078,27 @@ impl BdsApp {
}
fn insert_markdown_into_post(&mut self, post_id: &str, markdown: &str) -> Task<Message> {
let insertion_point = match self.active_modal.as_ref() {
Some(modal::ModalState::PostInsertLink {
post_id: modal_post_id,
insertion_point,
..
})
| Some(modal::ModalState::InsertMedia {
post_id: modal_post_id,
insertion_point,
..
}) if modal_post_id == post_id => Some(*insertion_point),
_ => None,
};
let Some(state) = self.post_editors.get_mut(post_id) else {
return Task::none();
};
state.insert_markdown_at_cursor(markdown);
if let Some(insertion_point) = insertion_point {
state.insert_markdown_at(markdown, insertion_point);
} else {
state.insert_markdown_at_cursor(markdown);
}
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == post_id) {
tab.is_dirty = true;
}
@@ -7228,6 +7247,7 @@ impl BdsApp {
let Some(modal::ModalState::PostInsertLink {
post_id,
title,
insertion_point,
search_query: current_query,
active_tab: current_tab,
external_url: current_url,
@@ -7246,6 +7266,7 @@ impl BdsApp {
self.active_modal = Some(modal::ModalState::PostInsertLink {
post_id: post_id.clone(),
title,
insertion_point,
results: self.query_post_link_results(&post_id, &next_query),
search_query: next_query,
active_tab: next_tab,
@@ -7258,6 +7279,7 @@ impl BdsApp {
let Some(modal::ModalState::InsertMedia {
post_id,
title,
insertion_point,
link_only,
..
}) = self.active_modal.clone()
@@ -7268,6 +7290,7 @@ impl BdsApp {
self.active_modal = Some(modal::ModalState::InsertMedia {
post_id,
title,
insertion_point,
media_list: self.query_post_insert_media_results(&search_query),
search_query,
link_only,
@@ -13542,6 +13565,31 @@ mod tests {
);
}
#[test]
fn link_modal_inserts_at_the_cursor_saved_before_focus_changes() {
let (mut app, post_id, _tmp) = auto_translation_test_app(&[]);
app.post_editors
.get_mut(&post_id)
.unwrap()
.editor_buffer
.borrow_mut()
.set_cursor(0, 2);
let _ = app.insert_link_modal(&post_id);
app.post_editors
.get_mut(&post_id)
.unwrap()
.editor_buffer
.borrow_mut()
.set_cursor(0, 0);
let _ = app.insert_markdown_into_post(&post_id, "[Link](/target)");
assert_eq!(
app.post_editors.get(&post_id).unwrap().content,
"Bo[Link](/target)dy"
);
}
#[test]
fn post_editor_loads_existing_project_tags_for_partial_suggestions() {
let (db, project, tmp) = setup();

View File

@@ -13,7 +13,7 @@ use bds_core::util::paths::thumbnail_path;
use crate::app::Message;
use crate::components::inputs;
use crate::i18n::t;
use crate::views::post_editor::PostEditorMsg;
use crate::views::post_editor::{EditorInsertionPoint, PostEditorMsg};
#[derive(Debug, Clone)]
pub struct InsertLinkResult {
@@ -77,6 +77,7 @@ pub enum ModalState {
PostInsertLink {
post_id: String,
title: String,
insertion_point: EditorInsertionPoint,
results: Vec<InsertLinkResult>,
search_query: String,
active_tab: PostInsertLinkTab,
@@ -87,6 +88,7 @@ pub enum ModalState {
InsertMedia {
post_id: String,
title: String,
insertion_point: EditorInsertionPoint,
media_list: Vec<bds_core::model::Media>,
search_query: String,
link_only: bool,
@@ -598,6 +600,7 @@ pub fn view(
ModalState::PostInsertLink {
post_id: _post_id,
title: _title,
insertion_point: _,
results,
search_query,
active_tab,
@@ -820,6 +823,7 @@ pub fn view(
ModalState::InsertMedia {
post_id: _post_id,
title: _title,
insertion_point: _,
media_list,
search_query,
link_only: _,

View File

@@ -9,7 +9,7 @@ use iced::{Color, Element, Length, Theme};
use bds_core::i18n::{self, UiLocale};
use bds_core::model::{Post, PostStatus, PostTranslation};
use bds_editor::{CodeEditor, EditorBuffer, EditorMessage, highlighter};
use bds_editor::{CodeEditor, EditorBuffer, EditorMessage, Selection, highlighter};
use crate::app::Message;
use crate::components::{inputs, popover};
@@ -52,6 +52,12 @@ pub struct LinkedMediaItem {
pub sort_order: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EditorInsertionPoint {
cursor: (usize, usize),
selection: Option<Selection>,
}
/// State for an open post editor.
pub struct PostEditorState {
pub post_id: String,
@@ -269,8 +275,30 @@ impl PostEditorState {
}
pub fn insert_markdown_at_cursor(&mut self, markdown: &str) {
self.insert_markdown_at(markdown, self.insertion_point());
}
pub fn insertion_point(&self) -> EditorInsertionPoint {
let buffer = self.editor_buffer.borrow();
EditorInsertionPoint {
cursor: buffer.cursor(),
selection: buffer.selection().copied(),
}
}
pub fn insert_markdown_at(&mut self, markdown: &str, point: EditorInsertionPoint) {
let new_content = {
let mut buffer = self.editor_buffer.borrow_mut();
buffer.clear_selection();
buffer.set_cursor(point.cursor.0, point.cursor.1);
if let Some(selection) = point.selection {
buffer.set_selection(
selection.anchor_line,
selection.anchor_col,
selection.head_line,
selection.head_col,
);
}
buffer.insert(markdown);
buffer.text()
};