feat(lsp): drive checkbox commands from the wiki listsyms palette
Parse editor and export documents with each wiki's configured listsyms, and make toggle/cycle/reject plus parent-propagation walk the real palette glyphs instead of the hardcoded ' .oOX'. Falls back to the default palette when no wiki matches the URI. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -238,7 +238,7 @@ fn file_delete(args: Vec<Value>) -> Result<Option<WorkspaceEdit>, String> {
|
|||||||
fn list_checkbox(
|
fn list_checkbox(
|
||||||
backend: &Backend,
|
backend: &Backend,
|
||||||
args: Vec<Value>,
|
args: Vec<Value>,
|
||||||
mutate: fn(&str) -> Option<&'static str>,
|
mutate: fn(&nuwiki_core::listsyms::ListSyms, &str) -> Option<String>,
|
||||||
) -> Result<Option<WorkspaceEdit>, String> {
|
) -> Result<Option<WorkspaceEdit>, String> {
|
||||||
let p = parse_pos(args)?;
|
let p = parse_pos(args)?;
|
||||||
let doc = match backend.documents.get(&p.uri) {
|
let doc = match backend.documents.get(&p.uri) {
|
||||||
@@ -247,12 +247,14 @@ fn list_checkbox(
|
|||||||
};
|
};
|
||||||
let utf8 = backend.use_utf8.load(Ordering::Relaxed);
|
let utf8 = backend.use_utf8.load(Ordering::Relaxed);
|
||||||
let (line, col) = nav::lsp_to_byte_pos(p.position, &doc.text, utf8);
|
let (line, col) = nav::lsp_to_byte_pos(p.position, &doc.text, utf8);
|
||||||
let propagate = backend
|
let wiki = backend.wiki_for_uri(&p.uri);
|
||||||
.wiki_for_uri(&p.uri)
|
let syms = wiki
|
||||||
.map(|w| w.config.listsyms_propagate)
|
.as_ref()
|
||||||
.unwrap_or(true);
|
.map(|w| nuwiki_core::listsyms::ListSyms::new(&w.config.listsyms))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let propagate = wiki.map(|w| w.config.listsyms_propagate).unwrap_or(true);
|
||||||
Ok(ops::checkbox_edit(
|
Ok(ops::checkbox_edit(
|
||||||
&doc.text, &doc.ast, &p.uri, line, col, utf8, mutate, propagate,
|
&doc.text, &doc.ast, &p.uri, line, col, utf8, &syms, mutate, propagate,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1139,9 +1141,9 @@ fn export_all(backend: &Backend, args: Vec<Value>, force: bool) -> Result<Option
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
let registry = std::sync::Arc::clone(&backend.registry);
|
let registry = std::sync::Arc::clone(&backend.registry);
|
||||||
let Some(plugin) = registry.get("vimwiki") else {
|
if registry.get("vimwiki").is_none() {
|
||||||
return Err("vimwiki syntax plugin missing".to_string());
|
return Err("vimwiki syntax plugin missing".to_string());
|
||||||
};
|
}
|
||||||
let files = crate::index::walk_wiki_files(&cfg.root);
|
let files = crate::index::walk_wiki_files(&cfg.root);
|
||||||
let mut exported: Vec<serde_json::Value> = Vec::new();
|
let mut exported: Vec<serde_json::Value> = Vec::new();
|
||||||
let mut skipped: Vec<serde_json::Value> = Vec::new();
|
let mut skipped: Vec<serde_json::Value> = Vec::new();
|
||||||
@@ -1160,7 +1162,10 @@ fn export_all(backend: &Backend, args: Vec<Value>, force: bool) -> Result<Option
|
|||||||
let Ok(text) = std::fs::read_to_string(path) else {
|
let Ok(text) = std::fs::read_to_string(path) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let ast = plugin.parse(&text);
|
let ast = nuwiki_core::syntax::vimwiki::VimwikiSyntax::new().parse_with_listsyms(
|
||||||
|
&text,
|
||||||
|
&nuwiki_core::listsyms::ListSyms::new(&cfg.listsyms),
|
||||||
|
);
|
||||||
(text, ast)
|
(text, ast)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1271,52 +1276,79 @@ pub mod ops {
|
|||||||
BlockNode, BlockquoteNode, CheckboxState, DocumentNode, HeadingNode, ListItemNode,
|
BlockNode, BlockquoteNode, CheckboxState, DocumentNode, HeadingNode, ListItemNode,
|
||||||
ListNode, Span,
|
ListNode, Span,
|
||||||
};
|
};
|
||||||
|
use nuwiki_core::listsyms::ListSyms;
|
||||||
use tower_lsp::lsp_types::Location;
|
use tower_lsp::lsp_types::Location;
|
||||||
|
|
||||||
/// Toggle ` ↔ X. Mid-states (`.`, `o`, `O`) snap forward to `X`.
|
/// The single glyph inside a `[g]` checkbox marker, or `None` when
|
||||||
/// Rejected (`-`) reverts to empty.
|
/// `marker` isn't a well-formed one-glyph marker.
|
||||||
pub fn toggle_state(current: &str) -> Option<&'static str> {
|
fn marker_glyph(marker: &str) -> Option<char> {
|
||||||
match current {
|
let inner = marker.strip_prefix('[')?.strip_suffix(']')?;
|
||||||
"[ ]" => Some("[X]"),
|
let mut chars = inner.chars();
|
||||||
"[X]" => Some("[ ]"),
|
let glyph = chars.next()?;
|
||||||
"[.]" | "[o]" | "[O]" => Some("[X]"),
|
if chars.next().is_some() {
|
||||||
"[-]" => Some("[ ]"),
|
return None;
|
||||||
_ => None,
|
}
|
||||||
|
Some(glyph)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn marker_for(glyph: char) -> String {
|
||||||
|
format!("[{glyph}]")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle empty ↔ done. Mid-states snap forward to done; rejected reverts
|
||||||
|
/// to empty.
|
||||||
|
pub fn toggle_state(syms: &ListSyms, current: &str) -> Option<String> {
|
||||||
|
let glyph = marker_glyph(current)?;
|
||||||
|
if glyph == syms.rejected_glyph() {
|
||||||
|
return Some(marker_for(syms.empty_glyph()));
|
||||||
|
}
|
||||||
|
let index = syms.index_of(glyph)?;
|
||||||
|
if index == syms.len() - 1 {
|
||||||
|
Some(marker_for(syms.empty_glyph()))
|
||||||
|
} else {
|
||||||
|
Some(marker_for(syms.done_glyph()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `[ ]` → `[.]` → `[o]` → `[O]` → `[X]` → `[ ]`. `[-]` → `[ ]`.
|
/// Advance one step through the palette, wrapping done → empty.
|
||||||
pub fn cycle_state(current: &str) -> Option<&'static str> {
|
/// Rejected reverts to empty.
|
||||||
match current {
|
pub fn cycle_state(syms: &ListSyms, current: &str) -> Option<String> {
|
||||||
"[ ]" => Some("[.]"),
|
let glyph = marker_glyph(current)?;
|
||||||
"[.]" => Some("[o]"),
|
if glyph == syms.rejected_glyph() {
|
||||||
"[o]" => Some("[O]"),
|
return Some(marker_for(syms.empty_glyph()));
|
||||||
"[O]" => Some("[X]"),
|
|
||||||
"[X]" => Some("[ ]"),
|
|
||||||
"[-]" => Some("[ ]"),
|
|
||||||
_ => None,
|
|
||||||
}
|
}
|
||||||
|
let index = syms.index_of(glyph)?;
|
||||||
|
let next = if index + 1 >= syms.len() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
index + 1
|
||||||
|
};
|
||||||
|
Some(marker_for(syms.glyph_at(next)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Exact inverse of [`cycle_state`] — used by the `glp` mapping
|
/// Exact inverse of [`cycle_state`] — used by the `glp` mapping
|
||||||
/// ("cycle the checkbox state backward").
|
/// ("cycle the checkbox state backward"). Rejected reverts to empty.
|
||||||
pub fn cycle_state_back(current: &str) -> Option<&'static str> {
|
pub fn cycle_state_back(syms: &ListSyms, current: &str) -> Option<String> {
|
||||||
match current {
|
let glyph = marker_glyph(current)?;
|
||||||
"[ ]" => Some("[X]"),
|
if glyph == syms.rejected_glyph() {
|
||||||
"[X]" => Some("[O]"),
|
return Some(marker_for(syms.empty_glyph()));
|
||||||
"[O]" => Some("[o]"),
|
|
||||||
"[o]" => Some("[.]"),
|
|
||||||
"[.]" => Some("[ ]"),
|
|
||||||
"[-]" => Some("[ ]"),
|
|
||||||
_ => None,
|
|
||||||
}
|
}
|
||||||
|
let index = syms.index_of(glyph)?;
|
||||||
|
let prev = if index == 0 {
|
||||||
|
syms.len() - 1
|
||||||
|
} else {
|
||||||
|
index - 1
|
||||||
|
};
|
||||||
|
Some(marker_for(syms.glyph_at(prev)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle the `[-]` "rejected" state.
|
/// Toggle the rejected state.
|
||||||
pub fn reject_state(current: &str) -> Option<&'static str> {
|
pub fn reject_state(syms: &ListSyms, current: &str) -> Option<String> {
|
||||||
match current {
|
let glyph = marker_glyph(current)?;
|
||||||
"[-]" => Some("[ ]"),
|
if glyph == syms.rejected_glyph() {
|
||||||
_ => Some("[-]"),
|
Some(marker_for(syms.empty_glyph()))
|
||||||
|
} else {
|
||||||
|
Some(marker_for(syms.rejected_glyph()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1328,18 +1360,19 @@ pub mod ops {
|
|||||||
line: u32,
|
line: u32,
|
||||||
col: u32,
|
col: u32,
|
||||||
utf8: bool,
|
utf8: bool,
|
||||||
mutate: fn(&str) -> Option<&'static str>,
|
syms: &ListSyms,
|
||||||
|
mutate: fn(&ListSyms, &str) -> Option<String>,
|
||||||
propagate: bool,
|
propagate: bool,
|
||||||
) -> Option<WorkspaceEdit> {
|
) -> Option<WorkspaceEdit> {
|
||||||
let path = find_path_to_item(ast, line, col)?;
|
let path = find_path_to_item(ast, line, col)?;
|
||||||
let leaf = *path.last()?;
|
let leaf = *path.last()?;
|
||||||
let cb_span = find_checkbox_span(text, leaf.span)?;
|
let cb_span = find_checkbox_span(text, leaf.span)?;
|
||||||
let cur = &text[cb_span.start.offset..cb_span.end.offset];
|
let cur = &text[cb_span.start.offset..cb_span.end.offset];
|
||||||
let new = mutate(cur)?;
|
let new = mutate(syms, cur)?;
|
||||||
let mut b = WorkspaceEditBuilder::new();
|
let mut b = WorkspaceEditBuilder::new();
|
||||||
b.edit(
|
b.edit(
|
||||||
uri.clone(),
|
uri.clone(),
|
||||||
text_edit_replace(cb_span, new.to_string(), text, utf8),
|
text_edit_replace(cb_span, new.clone(), text, utf8),
|
||||||
);
|
);
|
||||||
|
|
||||||
if propagate && path.len() > 1 {
|
if propagate && path.len() > 1 {
|
||||||
@@ -1348,14 +1381,15 @@ pub mod ops {
|
|||||||
// children (using the modified child's *new* state). Stops at
|
// children (using the modified child's *new* state). Stops at
|
||||||
// the first ancestor that has no checkbox.
|
// the first ancestor that has no checkbox.
|
||||||
let mut child_node: &ListItemNode = leaf;
|
let mut child_node: &ListItemNode = leaf;
|
||||||
let mut child_marker: &str = new;
|
let mut child_marker: String = new;
|
||||||
for parent in path.iter().rev().skip(1) {
|
for parent in path.iter().rev().skip(1) {
|
||||||
let parent_cb_span = match find_checkbox_span(text, parent.span) {
|
let parent_cb_span = match find_checkbox_span(text, parent.span) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => break,
|
None => break,
|
||||||
};
|
};
|
||||||
let parent_cur = &text[parent_cb_span.start.offset..parent_cb_span.end.offset];
|
let parent_cur = &text[parent_cb_span.start.offset..parent_cb_span.end.offset];
|
||||||
let new_parent = match compute_parent_marker(parent, child_node, child_marker) {
|
let new_parent =
|
||||||
|
match compute_parent_marker(syms, parent, child_node, &child_marker) {
|
||||||
Some(m) => m,
|
Some(m) => m,
|
||||||
None => break,
|
None => break,
|
||||||
};
|
};
|
||||||
@@ -1364,7 +1398,7 @@ pub mod ops {
|
|||||||
}
|
}
|
||||||
b.edit(
|
b.edit(
|
||||||
uri.clone(),
|
uri.clone(),
|
||||||
text_edit_replace(parent_cb_span, new_parent.to_string(), text, utf8),
|
text_edit_replace(parent_cb_span, new_parent.clone(), text, utf8),
|
||||||
);
|
);
|
||||||
child_node = parent;
|
child_node = parent;
|
||||||
child_marker = new_parent;
|
child_marker = new_parent;
|
||||||
@@ -1376,17 +1410,13 @@ pub mod ops {
|
|||||||
|
|
||||||
/// Convert a marker string like `"[X]"` to its progress rate. `-1`
|
/// Convert a marker string like `"[X]"` to its progress rate. `-1`
|
||||||
/// signals rejected (handled specially when averaging). Returns
|
/// signals rejected (handled specially when averaging). Returns
|
||||||
/// `None` for unrecognised markers.
|
/// `None` for markers whose glyph isn't in the palette.
|
||||||
fn marker_to_rate(marker: &str) -> Option<i32> {
|
fn marker_to_rate(syms: &ListSyms, marker: &str) -> Option<i32> {
|
||||||
match marker {
|
let glyph = marker_glyph(marker)?;
|
||||||
"[ ]" => Some(0),
|
if glyph == syms.rejected_glyph() {
|
||||||
"[.]" => Some(25),
|
return Some(-1);
|
||||||
"[o]" => Some(50),
|
|
||||||
"[O]" => Some(75),
|
|
||||||
"[X]" => Some(100),
|
|
||||||
"[-]" => Some(-1),
|
|
||||||
_ => None,
|
|
||||||
}
|
}
|
||||||
|
Some(syms.rate(syms.index_of(glyph)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn checkbox_state_to_rate(state: CheckboxState) -> i32 {
|
fn checkbox_state_to_rate(state: CheckboxState) -> i32 {
|
||||||
@@ -1401,25 +1431,9 @@ pub mod ops {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Inverse of `marker_to_rate`. `rate` is 0..=100 or `-1` (rejected).
|
/// Inverse of `marker_to_rate`. `rate` is 0..=100 or `-1` (rejected).
|
||||||
/// Mirrors vimwiki's `s:rate_to_state` for the standard 5-symbol
|
/// Mirrors vimwiki's `s:rate_to_state`, generalised over the palette.
|
||||||
/// `' .oOX'` palette.
|
fn rate_to_marker(syms: &ListSyms, rate: i32) -> String {
|
||||||
fn rate_to_marker(rate: i32) -> &'static str {
|
marker_for(syms.glyph_for_rate(rate))
|
||||||
if rate == -1 {
|
|
||||||
return "[-]";
|
|
||||||
}
|
|
||||||
if rate <= 0 {
|
|
||||||
return "[ ]";
|
|
||||||
}
|
|
||||||
if rate >= 100 {
|
|
||||||
return "[X]";
|
|
||||||
}
|
|
||||||
// n=5 symbols, so n-2=3. ceil(rate/100 * 3) ∈ {1,2,3}
|
|
||||||
let idx = ((rate as f64) / 100.0 * 3.0).ceil() as i32;
|
|
||||||
match idx {
|
|
||||||
1 => "[.]",
|
|
||||||
2 => "[o]",
|
|
||||||
_ => "[O]",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the new marker for `parent` given that `modified_child`
|
/// Compute the new marker for `parent` given that `modified_child`
|
||||||
@@ -1431,17 +1445,18 @@ pub mod ops {
|
|||||||
/// count as 100% for the average, but if *every* child is rejected
|
/// count as 100% for the average, but if *every* child is rejected
|
||||||
/// the parent itself becomes rejected.
|
/// the parent itself becomes rejected.
|
||||||
fn compute_parent_marker(
|
fn compute_parent_marker(
|
||||||
|
syms: &ListSyms,
|
||||||
parent: &ListItemNode,
|
parent: &ListItemNode,
|
||||||
modified_child: &ListItemNode,
|
modified_child: &ListItemNode,
|
||||||
modified_marker: &str,
|
modified_marker: &str,
|
||||||
) -> Option<&'static str> {
|
) -> Option<String> {
|
||||||
let sub = parent.sublist.as_ref()?;
|
let sub = parent.sublist.as_ref()?;
|
||||||
let mut sum: i32 = 0;
|
let mut sum: i32 = 0;
|
||||||
let mut count_with_cb: i32 = 0;
|
let mut count_with_cb: i32 = 0;
|
||||||
let mut count_rejected: i32 = 0;
|
let mut count_rejected: i32 = 0;
|
||||||
for item in &sub.items {
|
for item in &sub.items {
|
||||||
let rate = if std::ptr::eq(item, modified_child) {
|
let rate = if std::ptr::eq(item, modified_child) {
|
||||||
marker_to_rate(modified_marker)?
|
marker_to_rate(syms, modified_marker)?
|
||||||
} else {
|
} else {
|
||||||
match item.checkbox {
|
match item.checkbox {
|
||||||
Some(state) => checkbox_state_to_rate(state),
|
Some(state) => checkbox_state_to_rate(state),
|
||||||
@@ -1464,7 +1479,7 @@ pub mod ops {
|
|||||||
} else {
|
} else {
|
||||||
sum / count_with_cb
|
sum / count_with_cb
|
||||||
};
|
};
|
||||||
Some(rate_to_marker(new_rate))
|
Some(rate_to_marker(syms, new_rate))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_task(
|
pub fn next_task(
|
||||||
|
|||||||
@@ -48,9 +48,10 @@ pub struct WikiConfig {
|
|||||||
pub diary_sort: String,
|
pub diary_sort: String,
|
||||||
/// H1 text for the diary index page.
|
/// H1 text for the diary index page.
|
||||||
pub diary_header: String,
|
pub diary_header: String,
|
||||||
/// Vimwiki's `g:vimwiki_listsyms`. The 5-character string maps
|
/// Vimwiki's `g:vimwiki_listsyms`. A progression of checkbox glyphs
|
||||||
/// fractional progress states for checkboxes; the i-th char (0..=4)
|
/// from "empty" (first) to "done" (last); the lexer recognises these
|
||||||
/// is the rendered marker for state `i / 4`. Default: `" .oOX"`.
|
/// glyphs and the toggle/cycle commands walk them. Any length ≥ 2 is
|
||||||
|
/// honoured. Default: `" .oOX"`.
|
||||||
pub listsyms: String,
|
pub listsyms: String,
|
||||||
/// When toggling a parent list item, also cascade to descendants.
|
/// When toggling a parent list item, also cascade to descendants.
|
||||||
pub listsyms_propagate: bool,
|
pub listsyms_propagate: bool,
|
||||||
|
|||||||
@@ -187,6 +187,17 @@ impl Backend {
|
|||||||
id_or_default.and_then(|id| self.wiki(id))
|
id_or_default.and_then(|id| self.wiki(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse `text` as vimwiki, honouring the checkbox palette
|
||||||
|
/// (`listsyms`) of the wiki owning `uri`. Falls back to the default
|
||||||
|
/// palette when no wiki matches.
|
||||||
|
fn parse_for_uri(&self, uri: &Url, text: &str) -> nuwiki_core::ast::DocumentNode {
|
||||||
|
let syms = self
|
||||||
|
.wiki_for_uri(uri)
|
||||||
|
.map(|w| nuwiki_core::listsyms::ListSyms::new(&w.config.listsyms))
|
||||||
|
.unwrap_or_default();
|
||||||
|
nuwiki_core::syntax::vimwiki::VimwikiSyntax::new().parse_with_listsyms(text, &syms)
|
||||||
|
}
|
||||||
|
|
||||||
/// Snapshot of every registered wiki — held by value so callers can
|
/// Snapshot of every registered wiki — held by value so callers can
|
||||||
/// drop the lock before awaiting or building responses.
|
/// drop the lock before awaiting or building responses.
|
||||||
pub(crate) fn wikis_snapshot(&self) -> Vec<Wiki> {
|
pub(crate) fn wikis_snapshot(&self) -> Vec<Wiki> {
|
||||||
@@ -304,11 +315,10 @@ impl Backend {
|
|||||||
.to_file_path()
|
.to_file_path()
|
||||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "non-file URI"))?;
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "non-file URI"))?;
|
||||||
let text = tokio::fs::read_to_string(&path).await?;
|
let text = tokio::fs::read_to_string(&path).await?;
|
||||||
let plugin = self
|
if self.registry.get("vimwiki").is_none() {
|
||||||
.registry
|
return Err(io::Error::other("vimwiki syntax plugin missing"));
|
||||||
.get("vimwiki")
|
}
|
||||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "vimwiki syntax plugin missing"))?;
|
let ast = self.parse_for_uri(uri, &text);
|
||||||
let ast = plugin.parse(&text);
|
|
||||||
Ok(DocSnapshot {
|
Ok(DocSnapshot {
|
||||||
text,
|
text,
|
||||||
ast,
|
ast,
|
||||||
@@ -319,16 +329,13 @@ impl Backend {
|
|||||||
async fn update_document(&self, uri: Url, text: String, version: i32) {
|
async fn update_document(&self, uri: Url, text: String, version: i32) {
|
||||||
// We always treat unknown buffers as vimwiki — if/when
|
// We always treat unknown buffers as vimwiki — if/when
|
||||||
// multi-syntax dispatch lands the pick should follow the URI's ext.
|
// multi-syntax dispatch lands the pick should follow the URI's ext.
|
||||||
let plugin = match self.registry.get("vimwiki") {
|
if self.registry.get("vimwiki").is_none() {
|
||||||
Some(p) => p,
|
|
||||||
None => {
|
|
||||||
self.client
|
self.client
|
||||||
.log_message(MessageType::ERROR, "vimwiki plugin missing")
|
.log_message(MessageType::ERROR, "vimwiki plugin missing")
|
||||||
.await;
|
.await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
let ast = self.parse_for_uri(&uri, &text);
|
||||||
let ast = plugin.parse(&text);
|
|
||||||
let utf8 = self.use_utf8.load(Ordering::Relaxed);
|
let utf8 = self.use_utf8.load(Ordering::Relaxed);
|
||||||
|
|
||||||
// Compute diagnostics + update the matching wiki's index. All locks
|
// Compute diagnostics + update the matching wiki's index. All locks
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
//! emits `WorkspaceEdit`s for `nuwiki.list.toggleCheckbox`,
|
//! emits `WorkspaceEdit`s for `nuwiki.list.toggleCheckbox`,
|
||||||
//! `nuwiki.list.cycleCheckbox`, and `nuwiki.list.rejectCheckbox`.
|
//! `nuwiki.list.cycleCheckbox`, and `nuwiki.list.rejectCheckbox`.
|
||||||
|
|
||||||
|
use nuwiki_core::listsyms::ListSyms;
|
||||||
use nuwiki_core::syntax::vimwiki::VimwikiSyntax;
|
use nuwiki_core::syntax::vimwiki::VimwikiSyntax;
|
||||||
use nuwiki_core::syntax::SyntaxPlugin;
|
use nuwiki_core::syntax::SyntaxPlugin;
|
||||||
use nuwiki_lsp::commands::ops;
|
use nuwiki_lsp::commands::ops;
|
||||||
@@ -11,6 +12,10 @@ fn parse(src: &str) -> nuwiki_core::ast::DocumentNode {
|
|||||||
VimwikiSyntax::new().parse(src)
|
VimwikiSyntax::new().parse(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn syms() -> ListSyms {
|
||||||
|
ListSyms::default()
|
||||||
|
}
|
||||||
|
|
||||||
fn dummy_uri() -> Url {
|
fn dummy_uri() -> Url {
|
||||||
Url::parse("file:///tmp/note.wiki").unwrap()
|
Url::parse("file:///tmp/note.wiki").unwrap()
|
||||||
}
|
}
|
||||||
@@ -30,46 +35,65 @@ fn doc_changes_for(edit: tower_lsp::lsp_types::WorkspaceEdit) -> Option<Document
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn toggle_state_round_trip() {
|
fn toggle_state_round_trip() {
|
||||||
assert_eq!(ops::toggle_state("[ ]"), Some("[X]"));
|
let s = syms();
|
||||||
assert_eq!(ops::toggle_state("[X]"), Some("[ ]"));
|
assert_eq!(ops::toggle_state(&s, "[ ]").as_deref(), Some("[X]"));
|
||||||
assert_eq!(ops::toggle_state("[.]"), Some("[X]"));
|
assert_eq!(ops::toggle_state(&s, "[X]").as_deref(), Some("[ ]"));
|
||||||
assert_eq!(ops::toggle_state("[-]"), Some("[ ]"));
|
assert_eq!(ops::toggle_state(&s, "[.]").as_deref(), Some("[X]"));
|
||||||
assert_eq!(ops::toggle_state("[?]"), None);
|
assert_eq!(ops::toggle_state(&s, "[-]").as_deref(), Some("[ ]"));
|
||||||
|
assert_eq!(ops::toggle_state(&s, "[?]").as_deref(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cycle_state_walks_progression() {
|
fn cycle_state_walks_progression() {
|
||||||
assert_eq!(ops::cycle_state("[ ]"), Some("[.]"));
|
let s = syms();
|
||||||
assert_eq!(ops::cycle_state("[.]"), Some("[o]"));
|
assert_eq!(ops::cycle_state(&s, "[ ]").as_deref(), Some("[.]"));
|
||||||
assert_eq!(ops::cycle_state("[o]"), Some("[O]"));
|
assert_eq!(ops::cycle_state(&s, "[.]").as_deref(), Some("[o]"));
|
||||||
assert_eq!(ops::cycle_state("[O]"), Some("[X]"));
|
assert_eq!(ops::cycle_state(&s, "[o]").as_deref(), Some("[O]"));
|
||||||
assert_eq!(ops::cycle_state("[X]"), Some("[ ]"));
|
assert_eq!(ops::cycle_state(&s, "[O]").as_deref(), Some("[X]"));
|
||||||
assert_eq!(ops::cycle_state("[-]"), Some("[ ]"));
|
assert_eq!(ops::cycle_state(&s, "[X]").as_deref(), Some("[ ]"));
|
||||||
|
assert_eq!(ops::cycle_state(&s, "[-]").as_deref(), Some("[ ]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cycle_state_back_is_exact_inverse() {
|
fn cycle_state_back_is_exact_inverse() {
|
||||||
// `glp` walks the progression in reverse — every step must undo the
|
// `glp` walks the progression in reverse — every step must undo the
|
||||||
// matching `cycle_state` step.
|
// matching `cycle_state` step.
|
||||||
assert_eq!(ops::cycle_state_back("[ ]"), Some("[X]"));
|
let s = syms();
|
||||||
assert_eq!(ops::cycle_state_back("[X]"), Some("[O]"));
|
assert_eq!(ops::cycle_state_back(&s, "[ ]").as_deref(), Some("[X]"));
|
||||||
assert_eq!(ops::cycle_state_back("[O]"), Some("[o]"));
|
assert_eq!(ops::cycle_state_back(&s, "[X]").as_deref(), Some("[O]"));
|
||||||
assert_eq!(ops::cycle_state_back("[o]"), Some("[.]"));
|
assert_eq!(ops::cycle_state_back(&s, "[O]").as_deref(), Some("[o]"));
|
||||||
assert_eq!(ops::cycle_state_back("[.]"), Some("[ ]"));
|
assert_eq!(ops::cycle_state_back(&s, "[o]").as_deref(), Some("[.]"));
|
||||||
assert_eq!(ops::cycle_state_back("[-]"), Some("[ ]"));
|
assert_eq!(ops::cycle_state_back(&s, "[.]").as_deref(), Some("[ ]"));
|
||||||
|
assert_eq!(ops::cycle_state_back(&s, "[-]").as_deref(), Some("[ ]"));
|
||||||
// Composing forward then backward returns to the start for every
|
// Composing forward then backward returns to the start for every
|
||||||
// in-cycle marker.
|
// in-cycle marker.
|
||||||
for s in ["[ ]", "[.]", "[o]", "[O]", "[X]"] {
|
for m in ["[ ]", "[.]", "[o]", "[O]", "[X]"] {
|
||||||
let fwd = ops::cycle_state(s).unwrap();
|
let fwd = ops::cycle_state(&s, m).unwrap();
|
||||||
assert_eq!(ops::cycle_state_back(fwd), Some(s));
|
assert_eq!(ops::cycle_state_back(&s, &fwd).as_deref(), Some(m));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reject_state_toggles_dash_marker() {
|
fn reject_state_toggles_dash_marker() {
|
||||||
assert_eq!(ops::reject_state("[ ]"), Some("[-]"));
|
let s = syms();
|
||||||
assert_eq!(ops::reject_state("[X]"), Some("[-]"));
|
assert_eq!(ops::reject_state(&s, "[ ]").as_deref(), Some("[-]"));
|
||||||
assert_eq!(ops::reject_state("[-]"), Some("[ ]"));
|
assert_eq!(ops::reject_state(&s, "[X]").as_deref(), Some("[-]"));
|
||||||
|
assert_eq!(ops::reject_state(&s, "[-]").as_deref(), Some("[ ]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_palette_three_symbols() {
|
||||||
|
// A 3-glyph palette `" x✓"`: empty/half/done. `[x]` is the only
|
||||||
|
// intermediate; cycling walks ` `→`x`→`✓`→` `.
|
||||||
|
let s = ListSyms::new(" x✓");
|
||||||
|
assert_eq!(ops::cycle_state(&s, "[ ]").as_deref(), Some("[x]"));
|
||||||
|
assert_eq!(ops::cycle_state(&s, "[x]").as_deref(), Some("[✓]"));
|
||||||
|
assert_eq!(ops::cycle_state(&s, "[✓]").as_deref(), Some("[ ]"));
|
||||||
|
// Default-palette glyphs are not in this palette → no transition.
|
||||||
|
assert_eq!(ops::cycle_state(&s, "[o]").as_deref(), None);
|
||||||
|
// Toggle still maps empty→done, done→empty.
|
||||||
|
assert_eq!(ops::toggle_state(&s, "[ ]").as_deref(), Some("[✓]"));
|
||||||
|
assert_eq!(ops::toggle_state(&s, "[✓]").as_deref(), Some("[ ]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== checkbox_edit =====
|
// ===== checkbox_edit =====
|
||||||
@@ -78,7 +102,17 @@ fn reject_state_toggles_dash_marker() {
|
|||||||
fn toggle_empty_checkbox_to_done() {
|
fn toggle_empty_checkbox_to_done() {
|
||||||
let src = "- [ ] task\n";
|
let src = "- [ ] task\n";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 0, 4, true, ops::toggle_state, true)
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
0,
|
||||||
|
4,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::toggle_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.expect("toggle edit");
|
.expect("toggle edit");
|
||||||
let te = one_text_edit(edit);
|
let te = one_text_edit(edit);
|
||||||
assert_eq!(te.new_text, "[X]");
|
assert_eq!(te.new_text, "[X]");
|
||||||
@@ -90,7 +124,17 @@ fn toggle_empty_checkbox_to_done() {
|
|||||||
fn cycle_advances_partial_states() {
|
fn cycle_advances_partial_states() {
|
||||||
let src = "- [.] half\n";
|
let src = "- [.] half\n";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 0, 0, true, ops::cycle_state, true)
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::cycle_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.expect("cycle edit");
|
.expect("cycle edit");
|
||||||
let te = one_text_edit(edit);
|
let te = one_text_edit(edit);
|
||||||
assert_eq!(te.new_text, "[o]");
|
assert_eq!(te.new_text, "[o]");
|
||||||
@@ -100,7 +144,17 @@ fn cycle_advances_partial_states() {
|
|||||||
fn reject_replaces_empty_with_dash() {
|
fn reject_replaces_empty_with_dash() {
|
||||||
let src = "* [ ] thing\n";
|
let src = "* [ ] thing\n";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 0, 0, true, ops::reject_state, true)
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::reject_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.expect("reject edit");
|
.expect("reject edit");
|
||||||
let te = one_text_edit(edit);
|
let te = one_text_edit(edit);
|
||||||
assert_eq!(te.new_text, "[-]");
|
assert_eq!(te.new_text, "[-]");
|
||||||
@@ -111,7 +165,17 @@ fn checkbox_edit_returns_none_on_plain_list_item() {
|
|||||||
// No `[…]` at all → no edit.
|
// No `[…]` at all → no edit.
|
||||||
let src = "- plain item\n";
|
let src = "- plain item\n";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 0, 0, true, ops::toggle_state, true);
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::toggle_state,
|
||||||
|
true,
|
||||||
|
);
|
||||||
assert!(edit.is_none());
|
assert!(edit.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +183,17 @@ fn checkbox_edit_returns_none_on_plain_list_item() {
|
|||||||
fn checkbox_edit_returns_none_off_list() {
|
fn checkbox_edit_returns_none_off_list() {
|
||||||
let src = "just a paragraph\n";
|
let src = "just a paragraph\n";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 0, 0, true, ops::toggle_state, true);
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::toggle_state,
|
||||||
|
true,
|
||||||
|
);
|
||||||
assert!(edit.is_none());
|
assert!(edit.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +202,17 @@ fn checkbox_edit_finds_item_in_sublist() {
|
|||||||
let src = "- top\n - [ ] nested\n";
|
let src = "- top\n - [ ] nested\n";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
// Cursor on line 1, the nested item.
|
// Cursor on line 1, the nested item.
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 1, 5, true, ops::toggle_state, true)
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
1,
|
||||||
|
5,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::toggle_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.expect("edit on nested item");
|
.expect("edit on nested item");
|
||||||
let te = one_text_edit(edit);
|
let te = one_text_edit(edit);
|
||||||
assert_eq!(te.new_text, "[X]");
|
assert_eq!(te.new_text, "[X]");
|
||||||
@@ -141,8 +225,18 @@ fn checkbox_edit_finds_item_in_sublist() {
|
|||||||
fn checkbox_edit_uses_changes_map_not_document_changes() {
|
fn checkbox_edit_uses_changes_map_not_document_changes() {
|
||||||
let src = "- [ ] task\n";
|
let src = "- [ ] task\n";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit =
|
let edit = ops::checkbox_edit(
|
||||||
ops::checkbox_edit(src, &doc, &dummy_uri(), 0, 4, true, ops::toggle_state, true).unwrap();
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
0,
|
||||||
|
4,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::toggle_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert!(doc_changes_for(edit.clone()).is_none());
|
assert!(doc_changes_for(edit.clone()).is_none());
|
||||||
assert!(edit.changes.is_some());
|
assert!(edit.changes.is_some());
|
||||||
}
|
}
|
||||||
@@ -175,7 +269,17 @@ fn propagate_one_of_four_done_marks_parent_quarter() {
|
|||||||
- [ ] sub4
|
- [ ] sub4
|
||||||
";
|
";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 1, 6, true, ops::toggle_state, true)
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
1,
|
||||||
|
6,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::toggle_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.expect("toggle edit");
|
.expect("toggle edit");
|
||||||
let pairs = edits_by_line(edit);
|
let pairs = edits_by_line(edit);
|
||||||
assert_eq!(pairs, vec![(0, "[.]".into()), (1, "[X]".into())]);
|
assert_eq!(pairs, vec![(0, "[.]".into()), (1, "[X]".into())]);
|
||||||
@@ -192,7 +296,17 @@ fn propagate_two_of_four_done_marks_parent_half() {
|
|||||||
- [ ] sub4
|
- [ ] sub4
|
||||||
";
|
";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 2, 6, true, ops::toggle_state, true)
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
2,
|
||||||
|
6,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::toggle_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.expect("toggle edit");
|
.expect("toggle edit");
|
||||||
let pairs = edits_by_line(edit);
|
let pairs = edits_by_line(edit);
|
||||||
assert_eq!(pairs, vec![(0, "[o]".into()), (2, "[X]".into())]);
|
assert_eq!(pairs, vec![(0, "[o]".into()), (2, "[X]".into())]);
|
||||||
@@ -208,7 +322,17 @@ fn propagate_three_of_four_done_marks_parent_three_quarters() {
|
|||||||
- [ ] sub4
|
- [ ] sub4
|
||||||
";
|
";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 3, 6, true, ops::toggle_state, true)
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
3,
|
||||||
|
6,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::toggle_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.expect("toggle edit");
|
.expect("toggle edit");
|
||||||
let pairs = edits_by_line(edit);
|
let pairs = edits_by_line(edit);
|
||||||
assert_eq!(pairs, vec![(0, "[O]".into()), (3, "[X]".into())]);
|
assert_eq!(pairs, vec![(0, "[O]".into()), (3, "[X]".into())]);
|
||||||
@@ -224,7 +348,17 @@ fn propagate_all_done_marks_parent_done() {
|
|||||||
- [ ] sub4
|
- [ ] sub4
|
||||||
";
|
";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 4, 6, true, ops::toggle_state, true)
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
4,
|
||||||
|
6,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::toggle_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.expect("toggle edit");
|
.expect("toggle edit");
|
||||||
let pairs = edits_by_line(edit);
|
let pairs = edits_by_line(edit);
|
||||||
assert_eq!(pairs, vec![(0, "[X]".into()), (4, "[X]".into())]);
|
assert_eq!(pairs, vec![(0, "[X]".into()), (4, "[X]".into())]);
|
||||||
@@ -241,7 +375,17 @@ fn propagate_untoggle_drops_parent_back() {
|
|||||||
- [ ] sub4
|
- [ ] sub4
|
||||||
";
|
";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 1, 6, true, ops::toggle_state, true)
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
1,
|
||||||
|
6,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::toggle_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.expect("toggle edit");
|
.expect("toggle edit");
|
||||||
let pairs = edits_by_line(edit);
|
let pairs = edits_by_line(edit);
|
||||||
assert_eq!(pairs, vec![(0, "[ ]".into()), (1, "[ ]".into())]);
|
assert_eq!(pairs, vec![(0, "[ ]".into()), (1, "[ ]".into())]);
|
||||||
@@ -264,6 +408,7 @@ fn propagate_disabled_only_emits_leaf_edit() {
|
|||||||
1,
|
1,
|
||||||
6,
|
6,
|
||||||
true,
|
true,
|
||||||
|
&syms(),
|
||||||
ops::toggle_state,
|
ops::toggle_state,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -281,7 +426,17 @@ fn propagate_skips_parent_without_checkbox() {
|
|||||||
- [ ] sub2
|
- [ ] sub2
|
||||||
";
|
";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 1, 6, true, ops::toggle_state, true)
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
1,
|
||||||
|
6,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::toggle_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.expect("toggle edit");
|
.expect("toggle edit");
|
||||||
let pairs = edits_by_line(edit);
|
let pairs = edits_by_line(edit);
|
||||||
assert_eq!(pairs, vec![(1, "[X]".into())]);
|
assert_eq!(pairs, vec![(1, "[X]".into())]);
|
||||||
@@ -298,7 +453,17 @@ fn propagate_walks_multiple_levels() {
|
|||||||
- [ ] sub2
|
- [ ] sub2
|
||||||
";
|
";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 2, 8, true, ops::toggle_state, true)
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
2,
|
||||||
|
8,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::toggle_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.expect("toggle edit");
|
.expect("toggle edit");
|
||||||
let pairs = edits_by_line(edit);
|
let pairs = edits_by_line(edit);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -317,7 +482,17 @@ fn propagate_all_rejected_marks_parent_rejected() {
|
|||||||
";
|
";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
// Reject sub2 so both children are rejected.
|
// Reject sub2 so both children are rejected.
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 2, 6, true, ops::reject_state, true)
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
2,
|
||||||
|
6,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::reject_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.expect("reject edit");
|
.expect("reject edit");
|
||||||
let pairs = edits_by_line(edit);
|
let pairs = edits_by_line(edit);
|
||||||
assert_eq!(pairs, vec![(0, "[-]".into()), (2, "[-]".into())]);
|
assert_eq!(pairs, vec![(0, "[-]".into()), (2, "[-]".into())]);
|
||||||
@@ -332,7 +507,17 @@ fn propagate_rejected_sibling_counts_as_done_for_average() {
|
|||||||
- [ ] sub2
|
- [ ] sub2
|
||||||
";
|
";
|
||||||
let doc = parse(src);
|
let doc = parse(src);
|
||||||
let edit = ops::checkbox_edit(src, &doc, &dummy_uri(), 2, 6, true, ops::toggle_state, true)
|
let edit = ops::checkbox_edit(
|
||||||
|
src,
|
||||||
|
&doc,
|
||||||
|
&dummy_uri(),
|
||||||
|
2,
|
||||||
|
6,
|
||||||
|
true,
|
||||||
|
&syms(),
|
||||||
|
ops::toggle_state,
|
||||||
|
true,
|
||||||
|
)
|
||||||
.expect("toggle edit");
|
.expect("toggle edit");
|
||||||
let pairs = edits_by_line(edit);
|
let pairs = edits_by_line(edit);
|
||||||
assert_eq!(pairs, vec![(0, "[X]".into()), (2, "[X]".into())]);
|
assert_eq!(pairs, vec![(0, "[X]".into()), (2, "[X]".into())]);
|
||||||
|
|||||||
Reference in New Issue
Block a user