Skip to content

feat: optional full-block display text for block links - #40

Open
WilliamNurmi wants to merge 6 commits into
Moyf:masterfrom
WilliamNurmi:multi-line-block-alias
Open

feat: optional full-block display text for block links#40
WilliamNurmi wants to merge 6 commits into
Moyf:masterfrom
WilliamNurmi:multi-line-block-alias

Conversation

@WilliamNurmi

@WilliamNurmi WilliamNurmi commented Jun 12, 2026

Copy link
Copy Markdown

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):

  • Off (default): behavior is unchanged — the alias source is the same single line as before (the detected block-id line when copying an existing id; the first line of the block when generating a new id).
  • On: the alias can cover the whole block — soft-wrapped continuation lines are joined with spaces, and trailing ^id tokens 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 ^id line 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)

  • The trailing-id strip used a greedy /\^.*\s*$/ that ate content from the first caret of the line (value is 2^10value is 2). It now strips only a valid, whitespace-separated ^id token at the end of the line — matching what Obsidian actually parses as a block id.
  • The - list-prefix strip was unanchored (replace('- ', '')) and could mangle mid-line text like a - 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.

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.
@Moyf

Moyf commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Hi William, thanks for your PR!
I'll need some time to review it since I'm kinda busy these days. I hope you understand. :D

@WilliamNurmi

WilliamNurmi commented Jun 12, 2026

Copy link
Copy Markdown
Author

I'll need some time to review it since I'm kinda busy these days. I hope you understand. :D

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.

@Moyf

Moyf commented Jun 12, 2026

Copy link
Copy Markdown
Owner

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 Moyf left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ^iddetectBlockRange() 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 item

Cursor 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)

  1. Per-line prefix strip in extractBlockDisplayText only handles - . getBlockText() now joins lines from ordered / * / + lists, but the cleanup still only strips - [ ] and - . So 1. first item ^abc produces alias 1. first item while - first item ^abc produces first item. Probably want the strip set to mirror isListItemStart().
  2. Code-block fallback only matches ``` and $$. ~~~ fenced blocks won't trigger the fallback. This is consistent with the existing codeBlockDetect limitation, so I'm fine deferring — just flagging for awareness.
  3. i18n wording (zh / zh-tw) reads naturally to me; no change needed.

What I verified

  • vitest run on 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 = false the alias source is exactly the previous single-line one, so existing users see no change. The greedy /\^.*\s*$/ → whitespace-anchored \^id fix and the | strip do apply to the off path too, but those are correct bug fixes (2^10 and table-row pipes were genuinely broken before).

Once the ordered / * / + insertion-range issue is sorted, happy to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants