Preserve cursor position for modal insertions
This commit is contained in:
@@ -9,7 +9,7 @@ The project is under active development. Core blogging workflows are broadly ava
|
||||
## Available Features
|
||||
|
||||
- Native Iced desktop workspace with localized menus, tabs, anchored editor popovers, automatically paged post/media sidebars with locale-aware post dates, calendar months, and relative-dated entity lists, row deletion, dialogs, embedded Wry previews, a live Pico CSS theme editor, direct Preferences navigation, and per-project restart restoration of the active activity, shell visibility, and open editor tabs. Its shared task UI keeps queued work off blocking workers, shows every active local or remote task plus recent history, and supports cooperative cancellation without oversubscribing background work.
|
||||
- Post and translation authoring with change-aware draft/published/archive lifecycle, file-backed change discard, canonical draft reopening after manual translation edits, non-disruptive automatic translation, desktop archive/unarchive actions, in-place published-frontmatter updates, metadata, tags, categories, live link/backlink graphs, media, and batch gallery-image import.
|
||||
- Post and translation authoring with change-aware draft/published/archive lifecycle, file-backed change discard, canonical draft reopening after manual translation edits, non-disruptive automatic translation, desktop archive/unarchive actions, in-place published-frontmatter updates, metadata, tags, categories, cursor-preserving link and media insertion, live link/backlink graphs, media, and batch gallery-image import.
|
||||
- Media import including HEIC/HEIF decoding, q80 WebP thumbnails (plus q85 AI JPEGs), metadata translations, filters, validation, post assignment, and sequential drag-and-drop insertion into post editors.
|
||||
- WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping.
|
||||
- Post, Liquid template, and Lua script editing with dedicated syntax highlighting, explicit syntax-check feedback, change-aware published/draft lifecycle, normalized collision-safe template/script slug changes, reference-safe template renames, and publish-time enforcement of the bDS2 Liquid tag/filter/operator subset, using a custom Ropey/Syntect/Cosmic Text editor and the documented, bDS2-signature-compatible project-scoped [`bds` host API](docs/scripting/API_REFERENCE.md) across utilities, rendered macros, and Blogmark transforms, including airplane-gated Git sync.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: _,
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user