use std::sync::Arc;
use parking_lot::Mutex;
use anyhow::Result;
use crate::ListStyle;
use frontend::commands::{
document_editing_commands, document_formatting_commands, document_inspection_commands,
inline_element_commands, undo_redo_commands,
};
use unicode_segmentation::UnicodeSegmentation;
use crate::convert::{to_i64, to_usize};
use crate::events::DocumentEvent;
use crate::flow::{CellRange, FlowElement, SelectionKind, TableCellRef};
use crate::fragment::DocumentFragment;
use crate::inner::{CursorData, QueuedEvents, TextDocumentInner};
use crate::text_table::TextTable;
use crate::{BlockFormat, FrameFormat, MoveMode, MoveOperation, SelectionType, TextFormat};
use crate::document::get_main_frame_id;
fn max_cursor_position(stats: &frontend::document_inspection::DocumentStatsDto) -> usize {
let chars = to_usize(stats.character_count);
let blocks = to_usize(stats.block_count);
if blocks > 1 {
chars + blocks - 1
} else {
chars
}
}
pub struct TextCursor {
pub(crate) doc: Arc<Mutex<TextDocumentInner>>,
pub(crate) data: Arc<Mutex<CursorData>>,
}
impl Clone for TextCursor {
fn clone(&self) -> Self {
let (position, anchor) = {
let d = self.data.lock();
(d.position, d.anchor)
};
let data = {
let mut inner = self.doc.lock();
let data = Arc::new(Mutex::new(CursorData {
position,
anchor,
cell_selection_override: None,
}));
inner.cursors.push(Arc::downgrade(&data));
data
};
TextCursor {
doc: self.doc.clone(),
data,
}
}
}
impl TextCursor {
fn read_cursor(&self) -> (usize, usize) {
let d = self.data.lock();
(d.position, d.anchor)
}
fn finish_edit(
&self,
inner: &mut TextDocumentInner,
edit_pos: usize,
removed: usize,
new_pos: usize,
blocks_affected: usize,
) -> QueuedEvents {
self.finish_edit_ext(inner, edit_pos, removed, new_pos, blocks_affected, true)
}
fn finish_edit_ext(
&self,
inner: &mut TextDocumentInner,
edit_pos: usize,
removed: usize,
new_pos: usize,
blocks_affected: usize,
flow_may_change: bool,
) -> QueuedEvents {
let added = new_pos - edit_pos;
inner.adjust_cursors(edit_pos, removed, added);
{
let mut d = self.data.lock();
d.position = new_pos;
d.anchor = new_pos;
}
inner.modified = true;
inner.invalidate_text_cache();
inner.rehighlight_affected(edit_pos);
inner.queue_event(DocumentEvent::ContentsChanged {
position: edit_pos,
chars_removed: removed,
chars_added: added,
blocks_affected,
});
inner.check_block_count_changed();
if flow_may_change {
inner.check_flow_changed();
}
self.queue_undo_redo_event(inner)
}
pub fn position(&self) -> usize {
self.data.lock().position
}
pub fn anchor(&self) -> usize {
self.data.lock().anchor
}
pub fn has_selection(&self) -> bool {
let d = self.data.lock();
d.position != d.anchor
}
pub fn selection_start(&self) -> usize {
let d = self.data.lock();
d.position.min(d.anchor)
}
pub fn selection_end(&self) -> usize {
let d = self.data.lock();
d.position.max(d.anchor)
}
pub fn selected_text(&self) -> Result<String> {
let (pos, anchor) = self.read_cursor();
if pos == anchor {
return Ok(String::new());
}
let start = pos.min(anchor);
let len = pos.max(anchor) - start;
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetTextAtPositionDto {
position: to_i64(start),
length: to_i64(len),
};
let result = document_inspection_commands::get_text_at_position(&inner.ctx, &dto)?;
Ok(result.text)
}
pub fn clear_selection(&self) {
let mut d = self.data.lock();
d.anchor = d.position;
}
pub fn at_block_start(&self) -> bool {
let pos = self.position();
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
if let Ok(info) = document_inspection_commands::get_block_at_position(&inner.ctx, &dto) {
pos == to_usize(info.block_start)
} else {
false
}
}
pub fn at_block_end(&self) -> bool {
let pos = self.position();
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
if let Ok(info) = document_inspection_commands::get_block_at_position(&inner.ctx, &dto) {
pos == to_usize(info.block_start) + to_usize(info.block_length)
} else {
false
}
}
pub fn at_start(&self) -> bool {
self.data.lock().position == 0
}
pub fn at_end(&self) -> bool {
let pos = self.position();
let inner = self.doc.lock();
let stats = document_inspection_commands::get_document_stats(&inner.ctx).unwrap_or({
frontend::document_inspection::DocumentStatsDto {
character_count: 0,
word_count: 0,
block_count: 0,
frame_count: 0,
image_count: 0,
list_count: 0,
table_count: 0,
}
});
pos >= max_cursor_position(&stats)
}
pub fn block_number(&self) -> usize {
let pos = self.position();
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
document_inspection_commands::get_block_at_position(&inner.ctx, &dto)
.map(|info| to_usize(info.block_number))
.unwrap_or(0)
}
pub fn position_in_block(&self) -> usize {
let pos = self.position();
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
document_inspection_commands::get_block_at_position(&inner.ctx, &dto)
.map(|info| pos.saturating_sub(to_usize(info.block_start)))
.unwrap_or(0)
}
pub fn set_position(&self, position: usize, mode: MoveMode) {
let end = {
let inner = self.doc.lock();
document_inspection_commands::get_document_stats(&inner.ctx)
.map(|s| max_cursor_position(&s))
.unwrap_or(0)
};
let mut pos = position.min(end);
if mode == MoveMode::KeepAnchor {
let anchor = self.data.lock().anchor;
let pos_cell = self.table_cell_at(pos);
let anchor_cell = self.table_cell_at(anchor);
match (&pos_cell, &anchor_cell) {
(Some(tc), None) => {
let before = anchor < pos;
if let Some(boundary) = self.table_boundary_position(tc.table.id(), !before) {
pos = boundary;
}
}
(None, Some(tc)) => {
let before = pos < anchor;
if let Some(boundary) = self.table_boundary_position(tc.table.id(), !before) {
pos = boundary;
}
}
_ => {}
}
}
let mut d = self.data.lock();
d.position = pos;
if mode == MoveMode::MoveAnchor {
d.anchor = pos;
}
d.cell_selection_override = None;
}
pub fn move_position(&self, operation: MoveOperation, mode: MoveMode, n: usize) -> bool {
let old_pos = self.position();
let target = self.resolve_move(operation, n);
self.set_position(target, mode);
self.position() != old_pos
}
pub fn select(&self, selection: SelectionType) {
match selection {
SelectionType::Document => {
let end = {
let inner = self.doc.lock();
document_inspection_commands::get_document_stats(&inner.ctx)
.map(|s| max_cursor_position(&s))
.unwrap_or(0)
};
let mut d = self.data.lock();
d.anchor = 0;
d.position = end;
d.cell_selection_override = None;
}
SelectionType::BlockUnderCursor | SelectionType::LineUnderCursor => {
let pos = self.position();
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
if let Ok(info) =
document_inspection_commands::get_block_at_position(&inner.ctx, &dto)
{
let start = to_usize(info.block_start);
let end = start + to_usize(info.block_length);
drop(inner);
let mut d = self.data.lock();
d.anchor = start;
d.position = end;
d.cell_selection_override = None;
}
}
SelectionType::WordUnderCursor => {
let pos = self.position();
let (word_start, word_end) = self.find_word_boundaries(pos);
let mut d = self.data.lock();
d.anchor = word_start;
d.position = word_end;
d.cell_selection_override = None;
}
}
}
pub fn insert_text(&self, text: &str) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let dto = frontend::document_editing::InsertTextDto {
position: to_i64(pos),
anchor: to_i64(anchor),
text: text.into(),
};
let queued = {
let mut inner = self.doc.lock();
let result = match document_editing_commands::insert_text(
&inner.ctx,
Some(inner.stack_id),
&dto,
) {
Ok(r) => r,
Err(_) if pos != anchor => {
undo_redo_commands::begin_composite(&inner.ctx, Some(inner.stack_id));
let del_dto = frontend::document_editing::DeleteTextDto {
position: to_i64(pos),
anchor: to_i64(anchor),
};
let del_result = document_editing_commands::delete_text(
&inner.ctx,
Some(inner.stack_id),
&del_dto,
)?;
let del_pos = to_usize(del_result.new_position);
let ins_dto = frontend::document_editing::InsertTextDto {
position: to_i64(del_pos),
anchor: to_i64(del_pos),
text: text.into(),
};
let ins_result = document_editing_commands::insert_text(
&inner.ctx,
Some(inner.stack_id),
&ins_dto,
)?;
undo_redo_commands::end_composite(&inner.ctx);
ins_result
}
Err(e) => return Err(e),
};
let edit_pos = pos.min(anchor);
let removed = pos.max(anchor) - edit_pos;
self.finish_edit_ext(
&mut inner,
edit_pos,
removed,
to_usize(result.new_position),
to_usize(result.blocks_affected),
false,
)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn insert_formatted_text(&self, text: &str, format: &TextFormat) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let make_dto = |p: usize, a: usize| frontend::document_editing::InsertFormattedTextDto {
position: to_i64(p),
anchor: to_i64(a),
text: text.into(),
font_family: format.font_family.clone().unwrap_or_default(),
font_point_size: format.font_point_size.map(|v| v as i64).unwrap_or(0),
font_bold: format.font_bold.unwrap_or(false),
font_italic: format.font_italic.unwrap_or(false),
font_underline: format.font_underline.unwrap_or(false),
font_strikeout: format.font_strikeout.unwrap_or(false),
};
let queued = {
let mut inner = self.doc.lock();
let result = match document_editing_commands::insert_formatted_text(
&inner.ctx,
Some(inner.stack_id),
&make_dto(pos, anchor),
) {
Ok(r) => r,
Err(_) if pos != anchor => {
undo_redo_commands::begin_composite(&inner.ctx, Some(inner.stack_id));
let del_dto = frontend::document_editing::DeleteTextDto {
position: to_i64(pos),
anchor: to_i64(anchor),
};
let del_result = document_editing_commands::delete_text(
&inner.ctx,
Some(inner.stack_id),
&del_dto,
)?;
let del_pos = to_usize(del_result.new_position);
let ins_result = document_editing_commands::insert_formatted_text(
&inner.ctx,
Some(inner.stack_id),
&make_dto(del_pos, del_pos),
)?;
undo_redo_commands::end_composite(&inner.ctx);
ins_result
}
Err(e) => return Err(e),
};
let edit_pos = pos.min(anchor);
let removed = pos.max(anchor) - edit_pos;
self.finish_edit_ext(
&mut inner,
edit_pos,
removed,
to_usize(result.new_position),
1,
false,
)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn insert_block(&self) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let queued = {
let mut inner = self.doc.lock();
let (insert_pos, removed) = if pos != anchor {
undo_redo_commands::begin_composite(&inner.ctx, Some(inner.stack_id));
let del_dto = frontend::document_editing::DeleteTextDto {
position: to_i64(pos),
anchor: to_i64(anchor),
};
let del_result = document_editing_commands::delete_text(
&inner.ctx,
Some(inner.stack_id),
&del_dto,
)?;
(
to_usize(del_result.new_position),
pos.max(anchor) - pos.min(anchor),
)
} else {
(pos, 0)
};
let dto = frontend::document_editing::InsertBlockDto {
position: to_i64(insert_pos),
anchor: to_i64(insert_pos),
};
let result =
document_editing_commands::insert_block(&inner.ctx, Some(inner.stack_id), &dto)?;
if pos != anchor {
undo_redo_commands::end_composite(&inner.ctx);
}
let edit_pos = pos.min(anchor);
self.finish_edit(
&mut inner,
edit_pos,
removed,
to_usize(result.new_position),
2,
)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn insert_html(&self, html: &str) -> Result<()> {
let frag = DocumentFragment::from_html(html);
self.insert_fragment(&frag)
}
pub fn insert_markdown(&self, markdown: &str) -> Result<()> {
let frag = DocumentFragment::from_markdown(markdown);
self.insert_fragment(&frag)
}
pub fn insert_fragment(&self, fragment: &DocumentFragment) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let queued = {
let mut inner = self.doc.lock();
let (insert_pos, removed) = if pos != anchor {
undo_redo_commands::begin_composite(&inner.ctx, Some(inner.stack_id));
let del_dto = frontend::document_editing::DeleteTextDto {
position: to_i64(pos),
anchor: to_i64(anchor),
};
let del_result = document_editing_commands::delete_text(
&inner.ctx,
Some(inner.stack_id),
&del_dto,
)?;
(
to_usize(del_result.new_position),
pos.max(anchor) - pos.min(anchor),
)
} else {
(pos, 0)
};
let dto = frontend::document_editing::InsertFragmentDto {
position: to_i64(insert_pos),
anchor: to_i64(insert_pos),
fragment_data: fragment.raw_data().into(),
};
let result =
document_editing_commands::insert_fragment(&inner.ctx, Some(inner.stack_id), &dto)?;
if pos != anchor {
undo_redo_commands::end_composite(&inner.ctx);
}
let edit_pos = pos.min(anchor);
self.finish_edit(
&mut inner,
edit_pos,
removed,
to_usize(result.new_position),
to_usize(result.blocks_added),
)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn selection(&self) -> DocumentFragment {
let (pos, anchor) = self.read_cursor();
let (extract_pos, extract_anchor) = match self.selection_kind() {
SelectionKind::Cells(ref range) => match self.cell_range_positions(range) {
Some((start, end)) => (start, end),
None => return DocumentFragment::new(),
},
SelectionKind::Mixed {
ref cell_range,
text_before,
text_after,
} => {
let (cell_start, cell_end) = match self.cell_range_positions(cell_range) {
Some(p) => p,
None => return DocumentFragment::new(),
};
let start = if text_before {
pos.min(anchor)
} else {
cell_start
};
let end = if text_after {
pos.max(anchor)
} else {
cell_end
};
(start.min(cell_start), end.max(cell_end))
}
SelectionKind::None => return DocumentFragment::new(),
SelectionKind::Text => (pos, anchor),
};
if extract_pos == extract_anchor {
return DocumentFragment::new();
}
let inner = self.doc.lock();
let dto = frontend::document_inspection::ExtractFragmentDto {
position: to_i64(extract_pos),
anchor: to_i64(extract_anchor),
};
match document_inspection_commands::extract_fragment(&inner.ctx, &dto) {
Ok(result) => DocumentFragment::from_raw(result.fragment_data, result.plain_text),
Err(_) => DocumentFragment::new(),
}
}
pub fn insert_image(&self, name: &str, width: u32, height: u32) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let queued = {
let mut inner = self.doc.lock();
let (insert_pos, removed) = if pos != anchor {
undo_redo_commands::begin_composite(&inner.ctx, Some(inner.stack_id));
let del_dto = frontend::document_editing::DeleteTextDto {
position: to_i64(pos),
anchor: to_i64(anchor),
};
let del_result = document_editing_commands::delete_text(
&inner.ctx,
Some(inner.stack_id),
&del_dto,
)?;
(
to_usize(del_result.new_position),
pos.max(anchor) - pos.min(anchor),
)
} else {
(pos, 0)
};
let dto = frontend::document_editing::InsertImageDto {
position: to_i64(insert_pos),
anchor: to_i64(insert_pos),
image_name: name.into(),
width: width as i64,
height: height as i64,
};
let result =
document_editing_commands::insert_image(&inner.ctx, Some(inner.stack_id), &dto)?;
if pos != anchor {
undo_redo_commands::end_composite(&inner.ctx);
}
let edit_pos = pos.min(anchor);
self.finish_edit_ext(
&mut inner,
edit_pos,
removed,
to_usize(result.new_position),
1,
false,
)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn insert_frame(&self) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::InsertFrameDto {
position: to_i64(pos),
anchor: to_i64(anchor),
};
document_editing_commands::insert_frame(&inner.ctx, Some(inner.stack_id), &dto)?;
inner.modified = true;
inner.invalidate_text_cache();
inner.rehighlight_affected(pos.min(anchor));
inner.queue_event(DocumentEvent::ContentsChanged {
position: pos.min(anchor),
chars_removed: 0,
chars_added: 0,
blocks_affected: 1,
});
inner.check_block_count_changed();
inner.check_flow_changed();
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn insert_table(&self, rows: usize, columns: usize) -> Result<TextTable> {
let (pos, anchor) = self.read_cursor();
let (table_id, queued) = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::InsertTableDto {
position: to_i64(pos),
anchor: to_i64(anchor),
rows: to_i64(rows),
columns: to_i64(columns),
};
let result =
document_editing_commands::insert_table(&inner.ctx, Some(inner.stack_id), &dto)?;
let new_pos = to_usize(result.new_position);
let table_id = to_usize(result.table_id);
inner.adjust_cursors(pos.min(anchor), 0, new_pos - pos.min(anchor));
{
let mut d = self.data.lock();
d.position = new_pos;
d.anchor = new_pos;
}
inner.modified = true;
inner.invalidate_text_cache();
inner.rehighlight_affected(pos.min(anchor));
inner.queue_event(DocumentEvent::ContentsChanged {
position: pos.min(anchor),
chars_removed: 0,
chars_added: new_pos - pos.min(anchor),
blocks_affected: 1,
});
inner.check_block_count_changed();
inner.check_flow_changed();
(table_id, self.queue_undo_redo_event(&mut inner))
};
crate::inner::dispatch_queued_events(queued);
Ok(TextTable {
doc: self.doc.clone(),
table_id,
})
}
pub fn current_table(&self) -> Option<TextTable> {
self.current_table_cell().map(|c| c.table)
}
pub fn current_table_cell(&self) -> Option<TableCellRef> {
let pos = self.position();
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
let block_info =
document_inspection_commands::get_block_at_position(&inner.ctx, &dto).ok()?;
let block_id = if to_i64(pos) < block_info.block_start && pos > 0 {
let prev_dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos - 1),
};
let prev_info =
document_inspection_commands::get_block_at_position(&inner.ctx, &prev_dto).ok()?;
prev_info.block_id as usize
} else {
block_info.block_id as usize
};
let block = crate::text_block::TextBlock {
doc: self.doc.clone(),
block_id,
};
drop(inner);
block.table_cell()
}
pub fn remove_table(&self, table_id: usize) -> Result<()> {
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::RemoveTableDto {
table_id: to_i64(table_id),
};
document_editing_commands::remove_table(&inner.ctx, Some(inner.stack_id), &dto)?;
inner.modified = true;
inner.invalidate_text_cache();
inner.rehighlight_all();
inner.check_block_count_changed();
inner.check_flow_changed();
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn insert_table_row(&self, table_id: usize, row_index: usize) -> Result<()> {
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::InsertTableRowDto {
table_id: to_i64(table_id),
row_index: to_i64(row_index),
};
document_editing_commands::insert_table_row(&inner.ctx, Some(inner.stack_id), &dto)?;
inner.modified = true;
inner.invalidate_text_cache();
inner.rehighlight_all();
inner.check_block_count_changed();
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn insert_table_column(&self, table_id: usize, column_index: usize) -> Result<()> {
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::InsertTableColumnDto {
table_id: to_i64(table_id),
column_index: to_i64(column_index),
};
document_editing_commands::insert_table_column(&inner.ctx, Some(inner.stack_id), &dto)?;
inner.modified = true;
inner.invalidate_text_cache();
inner.rehighlight_all();
inner.check_block_count_changed();
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn remove_table_row(&self, table_id: usize, row_index: usize) -> Result<()> {
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::RemoveTableRowDto {
table_id: to_i64(table_id),
row_index: to_i64(row_index),
};
document_editing_commands::remove_table_row(&inner.ctx, Some(inner.stack_id), &dto)?;
inner.modified = true;
inner.invalidate_text_cache();
inner.rehighlight_all();
inner.check_block_count_changed();
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn remove_table_column(&self, table_id: usize, column_index: usize) -> Result<()> {
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::RemoveTableColumnDto {
table_id: to_i64(table_id),
column_index: to_i64(column_index),
};
document_editing_commands::remove_table_column(&inner.ctx, Some(inner.stack_id), &dto)?;
inner.modified = true;
inner.invalidate_text_cache();
inner.rehighlight_all();
inner.check_block_count_changed();
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn merge_table_cells(
&self,
table_id: usize,
start_row: usize,
start_column: usize,
end_row: usize,
end_column: usize,
) -> Result<()> {
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::MergeTableCellsDto {
table_id: to_i64(table_id),
start_row: to_i64(start_row),
start_column: to_i64(start_column),
end_row: to_i64(end_row),
end_column: to_i64(end_column),
};
document_editing_commands::merge_table_cells(&inner.ctx, Some(inner.stack_id), &dto)?;
inner.modified = true;
inner.invalidate_text_cache();
inner.rehighlight_all();
inner.check_block_count_changed();
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn split_table_cell(
&self,
cell_id: usize,
split_rows: usize,
split_columns: usize,
) -> Result<()> {
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::SplitTableCellDto {
cell_id: to_i64(cell_id),
split_rows: to_i64(split_rows),
split_columns: to_i64(split_columns),
};
document_editing_commands::split_table_cell(&inner.ctx, Some(inner.stack_id), &dto)?;
inner.modified = true;
inner.invalidate_text_cache();
inner.rehighlight_all();
inner.check_block_count_changed();
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn set_table_format(
&self,
table_id: usize,
format: &crate::flow::TableFormat,
) -> Result<()> {
let queued = {
let mut inner = self.doc.lock();
let dto = format.to_set_dto(table_id);
document_formatting_commands::set_table_format(&inner.ctx, Some(inner.stack_id), &dto)?;
inner.modified = true;
inner.queue_event(DocumentEvent::FormatChanged {
position: 0,
length: 0,
kind: crate::flow::FormatChangeKind::Block,
});
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn set_table_cell_format(
&self,
cell_id: usize,
format: &crate::flow::CellFormat,
) -> Result<()> {
let queued = {
let mut inner = self.doc.lock();
let dto = format.to_set_dto(cell_id);
document_formatting_commands::set_table_cell_format(
&inner.ctx,
Some(inner.stack_id),
&dto,
)?;
inner.modified = true;
inner.queue_event(DocumentEvent::FormatChanged {
position: 0,
length: 0,
kind: crate::flow::FormatChangeKind::Block,
});
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn remove_current_table(&self) -> Result<()> {
let table = self
.current_table()
.ok_or_else(|| anyhow::anyhow!("cursor is not inside a table"))?;
self.remove_table(table.id())
}
pub fn insert_row_above(&self) -> Result<()> {
let cell_ref = self
.current_table_cell()
.ok_or_else(|| anyhow::anyhow!("cursor is not inside a table"))?;
self.insert_table_row(cell_ref.table.id(), cell_ref.row)
}
pub fn insert_row_below(&self) -> Result<()> {
let cell_ref = self
.current_table_cell()
.ok_or_else(|| anyhow::anyhow!("cursor is not inside a table"))?;
self.insert_table_row(cell_ref.table.id(), cell_ref.row + 1)
}
pub fn insert_column_before(&self) -> Result<()> {
let cell_ref = self
.current_table_cell()
.ok_or_else(|| anyhow::anyhow!("cursor is not inside a table"))?;
self.insert_table_column(cell_ref.table.id(), cell_ref.column)
}
pub fn insert_column_after(&self) -> Result<()> {
let cell_ref = self
.current_table_cell()
.ok_or_else(|| anyhow::anyhow!("cursor is not inside a table"))?;
self.insert_table_column(cell_ref.table.id(), cell_ref.column + 1)
}
pub fn remove_current_row(&self) -> Result<()> {
let cell_ref = self
.current_table_cell()
.ok_or_else(|| anyhow::anyhow!("cursor is not inside a table"))?;
self.remove_table_row(cell_ref.table.id(), cell_ref.row)
}
pub fn remove_current_column(&self) -> Result<()> {
let cell_ref = self
.current_table_cell()
.ok_or_else(|| anyhow::anyhow!("cursor is not inside a table"))?;
self.remove_table_column(cell_ref.table.id(), cell_ref.column)
}
pub fn merge_selected_cells(&self) -> Result<()> {
let pos_cell = self
.current_table_cell()
.ok_or_else(|| anyhow::anyhow!("cursor position is not inside a table"))?;
let (_pos, anchor) = self.read_cursor();
let anchor_cell = {
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(anchor),
};
let block_info = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)
.map_err(|_| anyhow::anyhow!("cursor anchor is not inside a table"))?;
let block = crate::text_block::TextBlock {
doc: self.doc.clone(),
block_id: block_info.block_id as usize,
};
drop(inner);
block
.table_cell()
.ok_or_else(|| anyhow::anyhow!("cursor anchor is not inside a table"))?
};
if pos_cell.table.id() != anchor_cell.table.id() {
return Err(anyhow::anyhow!(
"position and anchor are in different tables"
));
}
let start_row = pos_cell.row.min(anchor_cell.row);
let start_col = pos_cell.column.min(anchor_cell.column);
let end_row = pos_cell.row.max(anchor_cell.row);
let end_col = pos_cell.column.max(anchor_cell.column);
self.merge_table_cells(pos_cell.table.id(), start_row, start_col, end_row, end_col)
}
pub fn split_current_cell(&self, split_rows: usize, split_columns: usize) -> Result<()> {
let cell_ref = self
.current_table_cell()
.ok_or_else(|| anyhow::anyhow!("cursor is not inside a table"))?;
let cell = cell_ref
.table
.cell(cell_ref.row, cell_ref.column)
.ok_or_else(|| anyhow::anyhow!("cell not found"))?;
self.split_table_cell(cell.id(), split_rows, split_columns)
}
pub fn set_current_table_format(&self, format: &crate::flow::TableFormat) -> Result<()> {
let table = self
.current_table()
.ok_or_else(|| anyhow::anyhow!("cursor is not inside a table"))?;
self.set_table_format(table.id(), format)
}
pub fn set_current_cell_format(&self, format: &crate::flow::CellFormat) -> Result<()> {
let cell_ref = self
.current_table_cell()
.ok_or_else(|| anyhow::anyhow!("cursor is not inside a table"))?;
let cell = cell_ref
.table
.cell(cell_ref.row, cell_ref.column)
.ok_or_else(|| anyhow::anyhow!("cell not found"))?;
self.set_table_cell_format(cell.id(), format)
}
pub fn selection_kind(&self) -> crate::flow::SelectionKind {
use crate::flow::{CellRange, SelectionKind};
{
let d = self.data.lock();
if let Some(ref range) = d.cell_selection_override {
return SelectionKind::Cells(range.clone());
}
if d.position == d.anchor {
return SelectionKind::None;
}
}
let (pos, anchor) = self.read_cursor();
let pos_cell = self.table_cell_at(pos);
let anchor_cell = self.table_cell_at(anchor);
match (&pos_cell, &anchor_cell) {
(None, None) => {
let (start, end) = (pos.min(anchor), pos.max(anchor));
if let Some(t) = self.find_table_between(start, end) {
let table_id = t.id();
let rows = t.rows();
let cols = t.columns();
let range = CellRange {
table_id,
start_row: 0,
start_col: 0,
end_row: if rows > 0 { rows - 1 } else { 0 },
end_col: if cols > 0 { cols - 1 } else { 0 },
};
let spans = self.collect_cell_spans(table_id);
SelectionKind::Mixed {
cell_range: range.expand_for_spans(&spans),
text_before: true,
text_after: true,
}
} else {
SelectionKind::Text
}
}
(Some(pc), Some(ac)) => {
if pc.table.id() != ac.table.id() {
return SelectionKind::Text;
}
if pc.row == ac.row && pc.column == ac.column {
return SelectionKind::Text;
}
let range = CellRange {
table_id: pc.table.id(),
start_row: pc.row.min(ac.row),
start_col: pc.column.min(ac.column),
end_row: pc.row.max(ac.row),
end_col: pc.column.max(ac.column),
};
let spans = self.collect_cell_spans(pc.table.id());
SelectionKind::Cells(range.expand_for_spans(&spans))
}
(Some(tc), None) | (None, Some(tc)) => {
let table_id = tc.table.id();
let rows = tc.table.rows();
let cols = tc.table.columns();
let inside_pos = if pos_cell.is_some() { pos } else { anchor };
let outside_pos = if pos_cell.is_some() { anchor } else { pos };
let text_before = outside_pos < inside_pos;
let text_after = !text_before;
let range = CellRange {
table_id,
start_row: 0,
start_col: 0,
end_row: if rows > 0 { rows - 1 } else { 0 },
end_col: if cols > 0 { cols - 1 } else { 0 },
};
let spans = self.collect_cell_spans(table_id);
SelectionKind::Mixed {
cell_range: range.expand_for_spans(&spans),
text_before,
text_after,
}
}
}
}
pub fn is_cell_selection(&self) -> bool {
matches!(
self.selection_kind(),
crate::flow::SelectionKind::Cells(_) | crate::flow::SelectionKind::Mixed { .. }
)
}
pub fn selected_cell_range(&self) -> Option<crate::flow::CellRange> {
match self.selection_kind() {
crate::flow::SelectionKind::Cells(r) => Some(r),
crate::flow::SelectionKind::Mixed { cell_range, .. } => Some(cell_range),
_ => None,
}
}
pub fn selected_cells(&self) -> Vec<TableCellRef> {
let range = match self.selected_cell_range() {
Some(r) => r,
None => return Vec::new(),
};
let table = TextTable {
doc: self.doc.clone(),
table_id: range.table_id,
};
let mut cells = Vec::new();
for row in range.start_row..=range.end_row {
for col in range.start_col..=range.end_col {
if table.cell(row, col).is_some() {
cells.push(TableCellRef {
table: table.clone(),
row,
column: col,
});
}
}
}
cells
}
pub fn select_table_cell(&self, table_id: usize, row: usize, col: usize) {
let mut d = self.data.lock();
d.cell_selection_override = Some(crate::flow::CellRange {
table_id,
start_row: row,
start_col: col,
end_row: row,
end_col: col,
});
}
pub fn select_cell_range(
&self,
table_id: usize,
start_row: usize,
start_col: usize,
end_row: usize,
end_col: usize,
) {
let range = crate::flow::CellRange {
table_id,
start_row,
start_col,
end_row,
end_col,
};
let spans = self.collect_cell_spans(table_id);
let mut d = self.data.lock();
d.cell_selection_override = Some(range.expand_for_spans(&spans));
}
pub fn clear_cell_selection(&self) {
let mut d = self.data.lock();
d.cell_selection_override = None;
}
fn cell_range_positions(&self, range: &CellRange) -> Option<(usize, usize)> {
let inner = self.doc.lock();
let main_frame_id = get_main_frame_id(&inner);
let flow = crate::text_frame::build_flow_elements(&inner, &self.doc, main_frame_id);
drop(inner);
let table = flow.into_iter().find_map(|e| match e {
FlowElement::Table(t) if t.id() == range.table_id => Some(t),
_ => None,
})?;
let mut min_pos = usize::MAX;
let mut max_pos = 0usize;
for row in range.start_row..=range.end_row {
for col in range.start_col..=range.end_col {
if let Some(cell) = table.cell(row, col) {
for block in cell.blocks() {
let bp = block.position();
let bl = block.length();
min_pos = min_pos.min(bp);
max_pos = max_pos.max(bp + bl);
}
}
}
}
if min_pos == usize::MAX {
return None;
}
Some((min_pos, max_pos + 1))
}
fn table_cell_at(&self, position: usize) -> Option<TableCellRef> {
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(position),
};
let block_info =
document_inspection_commands::get_block_at_position(&inner.ctx, &dto).ok()?;
let block_id = if to_i64(position) < block_info.block_start && position > 0 {
let prev_dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(position - 1),
};
let prev_info =
document_inspection_commands::get_block_at_position(&inner.ctx, &prev_dto).ok()?;
prev_info.block_id as usize
} else {
block_info.block_id as usize
};
let block = crate::text_block::TextBlock {
doc: self.doc.clone(),
block_id,
};
drop(inner);
block.table_cell()
}
fn table_boundary_position(&self, table_id: usize, before: bool) -> Option<usize> {
let inner = self.doc.lock();
let main_frame_id = get_main_frame_id(&inner);
let flow = crate::text_frame::build_flow_elements(&inner, &self.doc, main_frame_id);
drop(inner);
let idx = flow
.iter()
.position(|e| matches!(e, FlowElement::Table(t) if t.id() == table_id))?;
if before {
for i in (0..idx).rev() {
if let FlowElement::Block(b) = &flow[i] {
return Some(b.position() + b.length());
}
}
} else {
for item in flow.iter().skip(idx + 1) {
if let FlowElement::Block(b) = item {
return Some(b.position());
}
}
}
None
}
fn find_table_between(&self, start: usize, end: usize) -> Option<TextTable> {
let inner = self.doc.lock();
let main_frame_id = get_main_frame_id(&inner);
let flow = crate::text_frame::build_flow_elements(&inner, &self.doc, main_frame_id);
drop(inner);
for elem in flow {
if let FlowElement::Table(t) = elem {
if let Some(first_cell) = t.cell(0, 0) {
let blocks = first_cell.blocks();
if let Some(fb) = blocks.first() {
let p = fb.position();
if p > start && p < end {
return Some(t);
}
}
}
}
}
None
}
fn collect_cell_spans(&self, table_id: usize) -> Vec<(usize, usize, usize, usize)> {
let inner = self.doc.lock();
let table_dto =
match frontend::commands::table_commands::get_table(&inner.ctx, &(table_id as u64))
.ok()
.flatten()
{
Some(t) => t,
None => return Vec::new(),
};
let mut spans = Vec::with_capacity(table_dto.cells.len());
for &cell_id in &table_dto.cells {
if let Some(cell) =
frontend::commands::table_cell_commands::get_table_cell(&inner.ctx, &cell_id)
.ok()
.flatten()
{
spans.push((
cell.row as usize,
cell.column as usize,
cell.row_span.max(1) as usize,
cell.column_span.max(1) as usize,
));
}
}
spans
}
pub fn delete_char(&self) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let (del_pos, del_anchor) = if pos != anchor {
(pos, anchor)
} else {
let end = {
let inner = self.doc.lock();
document_inspection_commands::get_document_stats(&inner.ctx)
.map(|s| max_cursor_position(&s))
.unwrap_or(0)
};
if pos >= end {
return Ok(());
}
(pos, pos + 1)
};
self.do_delete(del_pos, del_anchor)
}
pub fn delete_previous_char(&self) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let (del_pos, del_anchor) = if pos != anchor {
(pos, anchor)
} else if pos > 0 {
(pos - 1, pos)
} else {
return Ok(());
};
self.do_delete(del_pos, del_anchor)
}
pub fn remove_selected_text(&self) -> Result<String> {
let (pos, anchor) = self.read_cursor();
if pos == anchor {
return Ok(String::new());
}
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::DeleteTextDto {
position: to_i64(pos),
anchor: to_i64(anchor),
};
let result =
document_editing_commands::delete_text(&inner.ctx, Some(inner.stack_id), &dto)?;
let edit_pos = pos.min(anchor);
let removed = pos.max(anchor) - edit_pos;
let new_pos = to_usize(result.new_position);
inner.adjust_cursors(edit_pos, removed, 0);
{
let mut d = self.data.lock();
d.position = new_pos;
d.anchor = new_pos;
}
inner.modified = true;
inner.invalidate_text_cache();
inner.rehighlight_affected(edit_pos);
inner.queue_event(DocumentEvent::ContentsChanged {
position: edit_pos,
chars_removed: removed,
chars_added: 0,
blocks_affected: 1,
});
inner.check_block_count_changed();
inner.check_flow_changed();
(result.deleted_text, self.queue_undo_redo_event(&mut inner))
};
crate::inner::dispatch_queued_events(queued.1);
Ok(queued.0)
}
pub fn current_list(&self) -> Option<crate::TextList> {
let pos = self.position();
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
let block_info =
document_inspection_commands::get_block_at_position(&inner.ctx, &dto).ok()?;
let block = crate::text_block::TextBlock {
doc: self.doc.clone(),
block_id: block_info.block_id as usize,
};
drop(inner);
block.list()
}
pub fn create_list(&self, style: ListStyle) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::CreateListDto {
position: to_i64(pos),
anchor: to_i64(anchor),
style: style.clone(),
};
document_editing_commands::create_list(&inner.ctx, Some(inner.stack_id), &dto)?;
inner.modified = true;
inner.rehighlight_affected(pos.min(anchor));
inner.queue_event(DocumentEvent::ContentsChanged {
position: pos.min(anchor),
chars_removed: 0,
chars_added: 0,
blocks_affected: 1,
});
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn insert_list(&self, style: ListStyle) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::InsertListDto {
position: to_i64(pos),
anchor: to_i64(anchor),
style: style.clone(),
};
let result =
document_editing_commands::insert_list(&inner.ctx, Some(inner.stack_id), &dto)?;
let edit_pos = pos.min(anchor);
let removed = pos.max(anchor) - edit_pos;
self.finish_edit_ext(
&mut inner,
edit_pos,
removed,
to_usize(result.new_position),
1,
false,
)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn set_list_format(&self, list_id: usize, format: &crate::ListFormat) -> Result<()> {
let queued = {
let mut inner = self.doc.lock();
let dto = format.to_set_dto(list_id);
document_formatting_commands::set_list_format(&inner.ctx, Some(inner.stack_id), &dto)?;
inner.modified = true;
inner.queue_event(DocumentEvent::FormatChanged {
position: 0,
length: 0,
kind: crate::flow::FormatChangeKind::List,
});
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn set_current_list_format(&self, format: &crate::ListFormat) -> Result<()> {
let list = self
.current_list()
.ok_or_else(|| anyhow::anyhow!("cursor is not inside a list"))?;
self.set_list_format(list.id(), format)
}
pub fn add_block_to_list(&self, block_id: usize, list_id: usize) -> Result<()> {
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::AddBlockToListDto {
block_id: to_i64(block_id),
list_id: to_i64(list_id),
};
document_editing_commands::add_block_to_list(&inner.ctx, Some(inner.stack_id), &dto)?;
inner.modified = true;
inner.queue_event(DocumentEvent::ContentsChanged {
position: 0,
chars_removed: 0,
chars_added: 0,
blocks_affected: 1,
});
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn add_current_block_to_list(&self, list_id: usize) -> Result<()> {
let pos = self.position();
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
let block_info = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)?;
drop(inner);
self.add_block_to_list(block_info.block_id as usize, list_id)
}
pub fn remove_block_from_list(&self, block_id: usize) -> Result<()> {
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::RemoveBlockFromListDto {
block_id: to_i64(block_id),
};
document_editing_commands::remove_block_from_list(
&inner.ctx,
Some(inner.stack_id),
&dto,
)?;
inner.modified = true;
inner.queue_event(DocumentEvent::ContentsChanged {
position: 0,
chars_removed: 0,
chars_added: 0,
blocks_affected: 1,
});
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn remove_current_block_from_list(&self) -> Result<()> {
let pos = self.position();
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
let block_info = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)?;
drop(inner);
self.remove_block_from_list(block_info.block_id as usize)
}
pub fn remove_list_item(&self, list_id: usize, index: usize) -> Result<()> {
let list = crate::text_list::TextList {
doc: self.doc.clone(),
list_id,
};
let block = list
.item(index)
.ok_or_else(|| anyhow::anyhow!("list item index {index} out of range"))?;
self.remove_block_from_list(block.id())
}
pub fn char_format(&self) -> Result<TextFormat> {
let pos = self.position();
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetTextAtPositionDto {
position: to_i64(pos),
length: 1,
};
let text_info = document_inspection_commands::get_text_at_position(&inner.ctx, &dto)?;
let element_id = text_info.element_id as u64;
let element = inline_element_commands::get_inline_element(&inner.ctx, &element_id)?
.ok_or_else(|| anyhow::anyhow!("element not found at position"))?;
Ok(TextFormat::from(&element))
}
pub fn block_format(&self) -> Result<BlockFormat> {
let pos = self.position();
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
let block_info = document_inspection_commands::get_block_at_position(&inner.ctx, &dto)?;
let block_id = block_info.block_id as u64;
let block = frontend::commands::block_commands::get_block(&inner.ctx, &block_id)?
.ok_or_else(|| anyhow::anyhow!("block not found"))?;
Ok(BlockFormat::from(&block))
}
pub fn set_char_format(&self, format: &TextFormat) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let queued = {
let mut inner = self.doc.lock();
let dto = format.to_set_dto(pos, anchor);
document_formatting_commands::set_text_format(&inner.ctx, Some(inner.stack_id), &dto)?;
let start = pos.min(anchor);
let length = pos.max(anchor) - start;
inner.modified = true;
inner.queue_event(DocumentEvent::FormatChanged {
position: start,
length,
kind: crate::flow::FormatChangeKind::Character,
});
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn merge_char_format(&self, format: &TextFormat) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let queued = {
let mut inner = self.doc.lock();
let dto = format.to_merge_dto(pos, anchor);
document_formatting_commands::merge_text_format(
&inner.ctx,
Some(inner.stack_id),
&dto,
)?;
let start = pos.min(anchor);
let length = pos.max(anchor) - start;
inner.modified = true;
inner.queue_event(DocumentEvent::FormatChanged {
position: start,
length,
kind: crate::flow::FormatChangeKind::Character,
});
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn set_block_format(&self, format: &BlockFormat) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let queued = {
let mut inner = self.doc.lock();
let dto = format.to_set_dto(pos, anchor);
document_formatting_commands::set_block_format(&inner.ctx, Some(inner.stack_id), &dto)?;
let start = pos.min(anchor);
let length = pos.max(anchor) - start;
inner.modified = true;
inner.queue_event(DocumentEvent::FormatChanged {
position: start,
length,
kind: crate::flow::FormatChangeKind::Block,
});
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn set_frame_format(&self, frame_id: usize, format: &FrameFormat) -> Result<()> {
let (pos, anchor) = self.read_cursor();
let queued = {
let mut inner = self.doc.lock();
let dto = format.to_set_dto(pos, anchor, frame_id);
document_formatting_commands::set_frame_format(&inner.ctx, Some(inner.stack_id), &dto)?;
let start = pos.min(anchor);
let length = pos.max(anchor) - start;
inner.modified = true;
inner.queue_event(DocumentEvent::FormatChanged {
position: start,
length,
kind: crate::flow::FormatChangeKind::Block,
});
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
pub fn begin_edit_block(&self) {
let inner = self.doc.lock();
undo_redo_commands::begin_composite(&inner.ctx, Some(inner.stack_id));
}
pub fn end_edit_block(&self) {
let inner = self.doc.lock();
undo_redo_commands::end_composite(&inner.ctx);
}
pub fn join_previous_edit_block(&self) {
self.begin_edit_block();
}
fn queue_undo_redo_event(&self, inner: &mut TextDocumentInner) -> QueuedEvents {
let can_undo = undo_redo_commands::can_undo(&inner.ctx, Some(inner.stack_id));
let can_redo = undo_redo_commands::can_redo(&inner.ctx, Some(inner.stack_id));
inner.queue_event(DocumentEvent::UndoRedoChanged { can_undo, can_redo });
inner.take_queued_events()
}
fn do_delete(&self, pos: usize, anchor: usize) -> Result<()> {
let queued = {
let mut inner = self.doc.lock();
let dto = frontend::document_editing::DeleteTextDto {
position: to_i64(pos),
anchor: to_i64(anchor),
};
let result =
document_editing_commands::delete_text(&inner.ctx, Some(inner.stack_id), &dto)?;
let edit_pos = pos.min(anchor);
let removed = pos.max(anchor) - edit_pos;
let new_pos = to_usize(result.new_position);
inner.adjust_cursors(edit_pos, removed, 0);
{
let mut d = self.data.lock();
d.position = new_pos;
d.anchor = new_pos;
}
inner.modified = true;
inner.invalidate_text_cache();
inner.rehighlight_affected(edit_pos);
inner.queue_event(DocumentEvent::ContentsChanged {
position: edit_pos,
chars_removed: removed,
chars_added: 0,
blocks_affected: 1,
});
inner.check_block_count_changed();
inner.check_flow_changed();
self.queue_undo_redo_event(&mut inner)
};
crate::inner::dispatch_queued_events(queued);
Ok(())
}
fn resolve_move(&self, op: MoveOperation, n: usize) -> usize {
let pos = self.position();
match op {
MoveOperation::NoMove => pos,
MoveOperation::Start => 0,
MoveOperation::End => {
let inner = self.doc.lock();
document_inspection_commands::get_document_stats(&inner.ctx)
.map(|s| max_cursor_position(&s))
.unwrap_or(pos)
}
MoveOperation::NextCharacter | MoveOperation::Right => pos + n,
MoveOperation::PreviousCharacter | MoveOperation::Left => pos.saturating_sub(n),
MoveOperation::StartOfBlock | MoveOperation::StartOfLine => {
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
document_inspection_commands::get_block_at_position(&inner.ctx, &dto)
.map(|info| to_usize(info.block_start))
.unwrap_or(pos)
}
MoveOperation::EndOfBlock | MoveOperation::EndOfLine => {
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
document_inspection_commands::get_block_at_position(&inner.ctx, &dto)
.map(|info| to_usize(info.block_start) + to_usize(info.block_length))
.unwrap_or(pos)
}
MoveOperation::NextBlock => {
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
document_inspection_commands::get_block_at_position(&inner.ctx, &dto)
.map(|info| {
to_usize(info.block_start) + to_usize(info.block_length) + 1
})
.unwrap_or(pos)
}
MoveOperation::PreviousBlock => {
let inner = self.doc.lock();
let dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
let block_start =
document_inspection_commands::get_block_at_position(&inner.ctx, &dto)
.map(|info| to_usize(info.block_start))
.unwrap_or(pos);
if block_start >= 2 {
let prev_dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(block_start - 2),
};
document_inspection_commands::get_block_at_position(&inner.ctx, &prev_dto)
.map(|info| to_usize(info.block_start))
.unwrap_or(0)
} else {
0
}
}
MoveOperation::NextWord | MoveOperation::EndOfWord | MoveOperation::WordRight => {
let (_, end) = self.find_word_boundaries(pos);
if end == pos {
let inner = self.doc.lock();
let max_pos = document_inspection_commands::get_document_stats(&inner.ctx)
.map(|s| max_cursor_position(&s))
.unwrap_or(0);
let scan_len = max_pos.saturating_sub(pos).min(64);
if scan_len == 0 {
return pos;
}
let dto = frontend::document_inspection::GetTextAtPositionDto {
position: to_i64(pos),
length: to_i64(scan_len),
};
if let Ok(r) =
document_inspection_commands::get_text_at_position(&inner.ctx, &dto)
{
for (i, ch) in r.text.chars().enumerate() {
if ch.is_alphanumeric() || ch == '_' {
let word_pos = pos + i;
drop(inner);
let (_, word_end) = self.find_word_boundaries(word_pos);
return word_end;
}
}
}
pos + scan_len
} else {
end
}
}
MoveOperation::PreviousWord | MoveOperation::StartOfWord | MoveOperation::WordLeft => {
let (start, _) = self.find_word_boundaries(pos);
if start < pos {
start
} else if pos > 0 {
let mut search = pos - 1;
loop {
let (ws, we) = self.find_word_boundaries(search);
if ws < we {
break ws;
}
if search == 0 {
break 0;
}
search -= 1;
}
} else {
0
}
}
MoveOperation::Up | MoveOperation::Down => {
if matches!(op, MoveOperation::Up) {
self.resolve_move(MoveOperation::PreviousBlock, 1)
} else {
self.resolve_move(MoveOperation::NextBlock, 1)
}
}
}
}
fn find_word_boundaries(&self, pos: usize) -> (usize, usize) {
let inner = self.doc.lock();
let block_dto = frontend::document_inspection::GetBlockAtPositionDto {
position: to_i64(pos),
};
let block_info =
match document_inspection_commands::get_block_at_position(&inner.ctx, &block_dto) {
Ok(info) => info,
Err(_) => return (pos, pos),
};
let block_start = to_usize(block_info.block_start);
let block_length = to_usize(block_info.block_length);
if block_length == 0 {
return (pos, pos);
}
let dto = frontend::document_inspection::GetTextAtPositionDto {
position: to_i64(block_start),
length: to_i64(block_length),
};
let text = match document_inspection_commands::get_text_at_position(&inner.ctx, &dto) {
Ok(r) => r.text,
Err(_) => return (pos, pos),
};
let cursor_offset = pos.saturating_sub(block_start);
let mut last_char_start = 0;
let mut last_char_end = 0;
for (word_byte_start, word) in text.unicode_word_indices() {
let word_char_start = text[..word_byte_start].chars().count();
let word_char_len = word.chars().count();
let word_char_end = word_char_start + word_char_len;
last_char_start = word_char_start;
last_char_end = word_char_end;
if cursor_offset >= word_char_start && cursor_offset < word_char_end {
return (block_start + word_char_start, block_start + word_char_end);
}
}
if cursor_offset == last_char_end && last_char_start < last_char_end {
return (block_start + last_char_start, block_start + last_char_end);
}
(pos, pos)
}
}