Skip to content

fix: edit YAML lists (incl. tags) as native lists instead of collapsing them to a string (#94) - #128

Merged
chhoumann merged 2 commits into
masterfrom
chhoumann/94-multivalue
Jun 27, 2026
Merged

fix: edit YAML lists (incl. tags) as native lists instead of collapsing them to a string (#94)#128
chhoumann merged 2 commits into
masterfrom
chhoumann/94-multivalue

Conversation

@chhoumann

Copy link
Copy Markdown
Owner

Summary

The multi-value cluster (#94, #51, #31, #36) is one defect: MetaEdit lost the YAML list type when editing array properties (especially tags). Editing a tags/aliases list through the default All Single edit mode collapsed it into a comma-joined string.

Root cause: a real YAML list is inherently multi-value, but the editor chose its mode from the global single/multi flag, not from the value's structure. In All Single mode a list went through the single-line text editor, which flattened it. The element-aware list editor also round-tripped lists through join(", ") + re-split, shredding elements that contain commas or [[wikilinks]].

Fix

  • Structure drives the editor. A real YAML array always opens the element-aware list editor, regardless of EditMode (shouldUseMultiValueEditor). standardMode is untouched and still handles scalars.
  • Element-preserving edits. The list editor now operates on the original typed array and replaces only the touched element (applyMultiValueEdit), so every untouched element keeps its exact type, order, and spelling. No more join+resplit. YAML persists a real list via processFrontMatter (Obsidian's native block-list format); inline fields keep the comma-string convention.
  • Exact command matching. Add/insert sentinels are matched exactly, so a real element that merely contains "cmd" (e.g. cmd:build) is edited normally instead of collapsing the list (caught in adversarial review).
  • New pure, Obsidian-free src/multiValue.ts holds the routing + list logic so it is unit-tested in the jsdom-free node env.

What was already fixed (verified, not rebuilt)

On today's master, #36 (API update with an array), #51 (the write half), and #31 (list format) are already handled by the processFrontMatter write path from #119 - given a real array, Obsidian writes a native YAML list. Live evidence: api.update("foo", ["a","c"]) on foo: [a, b] -> foo:\n - a\n - c. The remaining defect was #94's edit-flow collapse. Regression tests lock all of them.

Closes

Closes #94. Addresses #51, #31, #36 (each reproduced live; the write-path ones were already fixed and are now covered by regression tests).

Validation

  • pnpm run lint (0 errors), pnpm run build, pnpm run test -> 171 unit tests pass (incl. new src/multiValue.test.ts and a parser.test.ts read-back case).
  • Live isolated Obsidian E2E (tests/e2e/multi-value.test.ts, 6 tests) driving the real suggester + prompt UI and processFrontMatter:
  • Full live E2E suite passes (29/29 on a clean run; the harness has pre-existing load-related obsidian-CLI timeout flakiness unrelated to this change - a different non-mine test times out on different runs and passes on retry).
  • Obsidian runtime: dev:errors clean. Obsidian 1.x, isolated worktree vault.

Adversarial review

Design and implementation were each reviewed by 3 opposing-model (Codex) reviewers. The design review rejected an initial toValueArray split-back approach (it corrupts commas/brackets/types); the current structure-drives-editor + element-preserving design is the result. The implementation review caught the cmd-substring collapse (fixed). Remaining reviewer notes are pre-existing, out-of-scope behaviors, surfaced below.

Out of scope (surfaced, not folded - separate features/workers)

  • API update with a string into an existing array stays a scalar - the documented "you get the type you pass" contract (maintainer's updateing a multi value property does not work. #36 position).
  • Bulk edit array handling, Auto-Property Single on an existing array, and create-time addYamlProp/addDataviewField comma handling each have their own type semantics in separate (recently merged) features.
  • Duplicate identical elements edit the first match - a pre-existing GenericSuggester limitation (returns the value, not the row); not regressed.

Release impact

No settings/storage/API changes. Behavior change: editing a YAML list now always opens the list editor (correct for a list); scalar editing is unchanged.

…string (#94)

A real YAML array is inherently multi-value, so it is now always edited with the
element-aware list editor regardless of the global EditMode. Previously the
default All Single mode routed a `tags`/`aliases` list through the single-line
editor, which flattened it into a comma-joined string and lost the list type.

The list editor is now element-preserving: it operates on the original typed
array and replaces only the element the user touched, so every untouched element
keeps its exact type, order, and spelling. This drops the old join-then-resplit
round-trip that shredded elements containing commas (`Smith, John`) or brackets
(`[[Home]]`). Add/insert sentinels are matched exactly so a real element that
merely contains "cmd" is not mistaken for a command. standardMode is unchanged
and still handles scalars.

The routing/list logic lives in a new pure `src/multiValue.ts`, unit-tested in
the jsdom-free node environment.
Drive the real suggester + prompt UI and the processFrontMatter write path:
- #94: editing a YAML `tags` list in the default All Single mode keeps it a native list
- #51: adding a tag via "Add to end" grows the native list
- editing a comma-bearing element does not shred the others
- a 'cmd'-containing element is edited, not mistaken for a command
- a YAML scalar still uses the single-line editor (no regression)
- #36: api.update with an array writes a native YAML list
@chhoumann
chhoumann merged commit 79f6ee0 into master Jun 27, 2026
7 checks passed
@chhoumann
chhoumann deleted the chhoumann/94-multivalue branch June 27, 2026 19:10

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f3efbf39e8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/metaController.ts
Comment on lines +265 to +268
const isAddCommand =
selectedOption === ADD_FIRST_ELEMENT ||
selectedOption === ADD_TO_BEGINNING ||
selectedOption === ADD_TO_END;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid treating literal command sentinels as edits

When a YAML array is now routed into this list editor from All Single mode, an existing element whose literal value is exactly cmd:addfirst, cmd:beg, or cmd:end collides with the internal add sentinels returned by GenericSuggester. Selecting that real element is interpreted as an add command instead of a replacement, and cmd:addfirst in a non-empty list will replace the whole list with the prompted value, dropping the other elements; track the selected row/index or use non-colliding item identities rather than reserving user-visible strings.

Useful? React with 👍 / 👎.

Comment thread src/metaController.ts
return false;
// YAML persists a real list (round-trips as a native YAML array); an
// inline/tag field stores the comma-joined string per the inline convention.
const newValue: unknown = property.type === MetaType.YAML ? newList : newList.join(", ");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Incorrect condition for determining output format. A YAML scalar edited in AllMulti mode will be written as an array instead of a string.

What breaks: When a YAML scalar property (e.g., status: open) is edited via multiValueMode in AllMulti or SomeMulti mode, the condition checks property.type === MetaType.YAML and writes newList (an array) directly, converting the scalar to an array ["new-value"].

Root cause: The condition uses property.type instead of editsArray. A YAML scalar reaches multiValueMode when EditMode is AllMulti/SomeMulti, where editsArray is false (scalar was split on commas), but property.type is still MetaType.YAML.

Fix:

const newValue: unknown = editsArray ? newList : newList.join(", ");

This ensures only properties that were originally arrays (editsArray === true) are written back as arrays, while scalars (even YAML ones) are joined back to strings.

Suggested change
const newValue: unknown = property.type === MetaType.YAML ? newList : newList.join(", ");
const newValue: unknown = editsArray ? newList : newList.join(", ");

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

chhoumann added a commit that referenced this pull request Jun 27, 2026
Re-verified the PR #119-#128 review findings on current master, fixed the confirmed regressions, documented the Auto Property cancel false positive, and added unit/live Obsidian regression coverage.
github-actions Bot pushed a commit that referenced this pull request Jul 1, 2026
# [1.9.0](1.8.4...1.9.0) (2026-07-01)

### Bug Fixes

* address review findings from PRs [#119](https://github.com/chhoumann/MetaEdit/issues/119)-[#128](https://github.com/chhoumann/MetaEdit/issues/128) ([adf0b57](adf0b57))
* **api:** preserve reserved keys when cloning values ([#162](#162)) ([cff6076](cff6076))
* **automators:** count only [x]/[X] tasks complete; null-guard Kanban lane display ([#145](#145)) ([21da53a](21da53a))
* **bulk:** reject __proto__/constructor as a bulk property key ([#148](#148)) ([f03eacc](f03eacc))
* **bulk:** serialize bulk frontmatter writes through the controller write queue ([#147](#147)) ([3eb8236](3eb8236))
* **controller:** guard __proto__/constructor in all frontmatter writes ([#159](#159)) ([9d99fae](9d99fae))
* **controller:** make inline-field writes fence-aware ([5f8e713](5f8e713))
* **core,api:** safe command no-op, block-list deletion, presence-based property lookups ([#143](#143)) ([1702d08](1702d08))
* edit YAML lists (incl. tags) as native lists instead of collapsing them to a string ([#94](#94)) ([#128](#128)) ([79f6ee0](79f6ee0)), closes [#51](#51) [#36](#36)
* harden frontmatter metadata writes ([add2e90](add2e90))
* Kanban helper syncs only the card's leading link, not trailing date/reference links ([#126](#126)) ([52aec1e](52aec1e))
* **kanban:** don't write to an ambiguous same-named note ([#158](#158)) ([7f9655b](7f9655b))
* **kanban:** single notice for a card missing the board property ([9b1c88a](9b1c88a))
* parse inline fields behind list/quote markers and brackets ([#122](#122)) ([a62ef21](a62ef21)), closes [#119](#119) [#18](#18) [#121](#121) [#78](#78) [#84](#84)
* **parser:** preserve [[wikilinks]] in multi-value inline edits ([c331adb](c331adb))
* **settings:** serialize Auto Property choice persistence to prevent lost updates ([#154](#154)) ([2066b3e](2066b3e))
* **settings:** settings tab no longer clobbers concurrently-added Auto Property choices ([5759534](5759534))
* **settings:** validate auto-properties before queueing and make rollbacks compare-and-restore ([#156](#156)) ([8cbacdc](8cbacdc)), closes [#154](#154)
* stop inline-field updates appending a stray bracket ([#127](#127)) ([d2617f5](d2617f5)), closes [#67](#67) [#121](#121)
* **suggester:** center row action icons flush-right ([#166](#166)) ([c00795e](c00795e))
* **suggester:** use native icons + tooltips for row actions ([70903cc](70903cc))
* **suggester:** well-formed duplicate Notice, robust modal close, cleaner name suggestions ([#144](#144)) ([915dd6b](915dd6b)), closes [#143](#143)
* **tags:** correct and clarify tag editing ([#142](#142)) ([b837899](b837899)), closes [#49](#49)
* tolerate malformed YAML frontmatter when parsing note metadata ([#132](#132)) ([b835f97](b835f97)), closes [#130](#130)

### Features

* adopt modern Obsidian APIs and raise baseline ([5131664](5131664))
* **api:** append Dataview field instances without replacing ([42c78a8](42c78a8)), closes [#91](#91)
* **api:** expand public integration surface ([#120](#120)) ([bbf0a7a](bbf0a7a))
* assist property-value entry with autocomplete and a native date picker ([#125](#125)) ([cd86235](cd86235)), closes [#61](#61) [#74](#74) [#61](#61) [#61](#61)
* **auto-properties:** description, multi-select, and learn-as-you-go values ([#123](#123)) ([4b24296](4b24296)), closes [#59](#59) [#40](#40) [#59](#59) [#40](#40) [#59](#59) [#40](#40) [#43](#43) [#30](#30)
* **auto-properties:** paste a list to split into choices ([#139](#139)) ([0245f7a](0245f7a))
* bulk edit metadata across folders and selected notes ([#124](#124)) ([c8d91e2](c8d91e2)), closes [#64](#64) [#20](#20)
* **create:** fluid, type-aware native YAML property creation ([#170](#170)) ([59b4c5d](59b4c5d))
* **editor:** edit properties with Obsidian's native widgets; raise minAppVersion to 1.12.7 ([#168](#168)) ([73b37e0](73b37e0))
* hide file tags from the Edit Meta menu ([#46](#46), [#90](#90)) ([#131](#131)) ([2c07937](2c07937)), closes [#tag](https://github.com/chhoumann/MetaEdit/issues/tag) [#tags](https://github.com/chhoumann/MetaEdit/issues/tags)
* support nested YAML path editing ([e9fce98](e9fce98))
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 1.9.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Metaedit doesn't edit arrays of tags properly.

1 participant