feat: optional full-block display text for block links - #40
Conversation
A soft-wrapped multi-line item is ONE block in Obsidian (the block id sits at the end of its last line and the cache span covers every line), but the auto display text was built from a single line: the range's first line when inserting a new id, the id-bearing last line when copying an existing one. Multi-line blocks therefore got truncated aliases. - new EasyCopy.getBlockText(): walks up to the block's first line (bullet or paragraph start, also from a continuation line under the cursor), extends through the soft-wrapped continuation lines, strips per-line trailing block ids, and joins with newlines - both copy paths pass the full block text; extractBlockDisplayText joins the lines with single spaces (blank lines dropped) before applying the existing word/char limits - the trailing-id strip is now ^[a-zA-Z0-9_-]+$ per line instead of the greedy /\^.*$/, so a mid-line caret (e.g. 2^10) survives - firstLine renamed blockText through the pipeline; 5 new tests
The previous commit changed the display-text source for block links from a single line to the whole multi-line block for everyone. Make the new behavior opt-in instead: - new setting "Use the whole block as display text" (blockDisplayFullBlock, default off). With the toggle off the display-text source is exactly the pre-existing single-line one: the detected block-id line when copying an existing id, the first line of the block when inserting a new id. - with the toggle on, tables, code blocks and math blocks fall back to the block id as display text instead of joining their markup into the alias (matching the old fallback for standalone-id forms). - strip | from the display text unconditionally: a pipe inside [[link|alias]] truncates the alias, and breaks the table row when the link is pasted into a table. This also affected the old single-line behavior when copying from table rows. Settings UI and i18n strings (EN/ZH/ZH-TW) included.
Review finding (pass 1): the full-block walk only recognized `- ` as a list-item start, so in ordered (`1. `/`1) `) and `* `/`+ ` lists the display text walked across neighboring items, even though each list item is its own block in Obsidian. Bound both the upward and the downward walk by a shared list-marker test, scoped to getBlockText so the default-off single-line path is untouched.
Review finding (pass 2): with the full-block setting on, the alias was expanded from the cursor line while detectBlockRange() (which only treats `- ` as a boundary) could detect a block id on a neighboring ordered/`* `/`+ ` list item — producing a link whose alias text comes from a different block than its target. Expand the alias from the line the id actually sits on (ContextData.line, set by detectBlockId) when copying, and from the insertion line (range end) when generating a new id. Detection and insertion behavior are unchanged.
Two review findings (pass 3) on the full-block text path: - a block id alone on its own line, one blank line below the content, belongs to the block above it (Obsidian's gap form). getBlockText() now jumps back to that block before expanding, so the alias carries the real block text instead of falling back to the block id. - the per-line trailing-id strip accepted a caret glued to content, eating e.g. the exponent of a line-final `2^10`. Obsidian only parses `^id` as a block id when it is whitespace-separated (or alone on the line) — the strip regex now requires the same.
Review finding (pass 4): with the full-block setting on, expanding the display text from a heading line carrying a block id (`## Intro ^abc` with a paragraph directly below) walked into the following paragraph, because the downward continuation scan only stops at blank/heading/list lines below the start. A heading is always its own single-line block, so the scan now skips extension entirely when the block starts with a heading.
|
Hi William, thanks for your PR! |
No worries, happy that you'll have a look at all :-). We made it as an option out of caution. If you don't see any risks / reasons not to have it on, let me know and I'll flip it default on or just default behaviour w/o an option so that aliases would just work for multi-line blocks. FYI for your review convenience: it's already ran through 5 rounds of codex review. |
This is so detailed, haha! Thank you for the reminder! ;) |
Moyf
left a comment
There was a problem hiding this comment.
Thanks for the thorough work and the multi-pass self-review — the layered fixes (mid-line caret, gap-separated id, heading-as-single-line) are well thought out. Tests pass locally (228 / 228). I think this is mergeable, but I'd like one fix before merging, plus a couple of small follow-ups.
Blocker (please address before merge)
Block-range detection still uses the old - -only logic when inserting a new block id.
getBlockText() correctly treats every list marker (- / * / + / 1. / 1)) as a block boundary via isListItemStart(). But the path that decides where to insert the new ^id — detectBlockRange() and isContinuousText() — still only recognizes - :
// src/main.ts (current)
private isContinuousText(line: string): boolean {
return line.trim() !== '' && !line.trim().startsWith('#') && !line.trim().startsWith('- ');
}Repro with blockDisplayFullBlock = on:
1. first item
2. second itemCursor on line 1 → detectBlockRange() walks end down to line 2 (2. second item is treated as continuation), so the ^id is inserted at the end of the second item. Meanwhile getBlockText(editor, end) then derives the alias from the second item too. Net result: the link's target and alias are both the second item, not the one the user is on. Same issue for * and + lists.
This contradicts the PR's stated invariant that "every list item is its own block". Suggest unifying the boundary logic — e.g. have detectBlockRange() / isContinuousText() reuse isListItemStart() so alias extraction, id insertion, and detectBlockId all share one definition of "block boundary". One test that pins the ordered / * / + insertion behavior would be enough to lock this down.
Non-blocking nits (fine as follow-ups)
- Per-line prefix strip in
extractBlockDisplayTextonly handles-.getBlockText()now joins lines from ordered /*/+lists, but the cleanup still only strips- [ ]and-. So1. first item ^abcproduces alias1. first itemwhile- first item ^abcproducesfirst item. Probably want the strip set to mirrorisListItemStart(). - Code-block fallback only matches
```and$$.~~~fenced blocks won't trigger the fallback. This is consistent with the existingcodeBlockDetectlimitation, so I'm fine deferring — just flagging for awareness. - i18n wording (zh / zh-tw) reads naturally to me; no change needed.
What I verified
vitest runon the PR branch: 228 passed (the 7 new cases cover the documented behaviors well).- Read every commit in the chain — the iterative fixes do address the issues each commit claims.
- Default-off behavior is preserved: with
blockDisplayFullBlock = falsethe alias source is exactly the previous single-line one, so existing users see no change. The greedy/\^.*\s*$/→ whitespace-anchored\^idfix and the|strip do apply to the off path too, but those are correct bug fixes (2^10and table-row pipes were genuinely broken before).
Once the ordered / * / + insertion-range issue is sorted, happy to merge.
What
When Generate display text for block links is enabled, the display text (alias) of a copied block link is taken from a single line. For soft-wrapped multi-line blocks — one logical block spanning several lines, common in vaults migrated from Roam or with long bullets — the alias covers only a fragment of the block.
This PR adds an opt-in setting "Use the whole block as display text" (
blockDisplayFullBlock, default off):^idtokens are stripped per line, and the existing word/char limits then apply to the joined text. Block boundaries follow Obsidian's semantics: every list item (-/*/+/ordered) is its own block, a heading is a single-line block, and a gap-separated^idline resolves to the block above it. Tables, code blocks and math blocks fall back to the block id as alias (the same fallback those forms had before). The alias is always expanded from the line the id actually sits on (or is inserted on), so the alias and the link target always agree.Display-text bug fixes (apply in both modes)
/\^.*\s*$/that ate content from the first caret of the line (value is 2^10→value is 2). It now strips only a valid, whitespace-separated^idtoken at the end of the line — matching what Obsidian actually parses as a block id.-list-prefix strip was unanchored (replace('- ', '')) and could mangle mid-line text likea - b; the list/task prefix strips are now anchored to the start of the line.|characters are stripped from the alias: a pipe inside[[link|alias]]truncates the alias, and breaks the row when the link is pasted into a table.Settings + i18n
New toggle in the block-id settings group (shown when display text generation is on). Strings included for EN / 简体中文 / 繁體中文 — please double-check the Chinese phrasing.
Tests
npx vitest run: 228 passing (7 new tests: multi-line joining, per-line id stripping, mid-line and end-of-line caret preservation, blank-line handling, word limits on joined text, pipe stripping). Manually verified in a live vault in both toggle states, including ordered lists, tables, code blocks, and gap-separated id lines.No version bump included — left to your release flow.