Skip to content

Stop lash add prepending above the H1 when the Tasks section is empty - #36

Merged
fohara merged 1 commit into
mainfrom
fix/add-empty-tasks-section
Aug 9, 2026
Merged

fohara merged 1 commit into
mainfrom
fix/add-empty-tasks-section

Conversation

@fohara

@fohara fohara commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

What was wrong

lash add against an existing file whose ## Tasks section held no tasks
wrote the checkbox at line 1, above the H1. The parser never saw it as a task,
so lash index reported 0 tasks and lash lint passed clean. The task was on
disk and nowhere else, with exit code 0 and a success message pointing at
path:0.

Repro: a well-formed file with a header and an empty ## Tasks section
(# T / @id: t / ## Tasks). The identical command against the same file
with one seed task appended correctly, which is what pinned the trigger to the
empty section.

resolve_append returned line_number: 0 whenever the file had no tasks,
commented "Signal for new file", and the emitter mapped 0 to insert index 0.
The sentinel conflated "brand-new file" with "existing file whose Tasks section
is empty", and only the first is safe to write at offset 0.

The fix

PlacementInfo now carries an InsertAnchor instead of a bare line number:
either a concrete Line(n) or EndOfTasksSection. The ambiguity cannot recur,
because there is no longer a number that means something other than a line.

Resolving EndOfTasksSection needs the source text. A parsed TaskFile
records task line numbers and nothing about section boundaries, so an empty
section has nothing to anchor to, which is why the old code fell back to a
hardcoded guess of line 15. The emitter resolves it instead, against content it
already reads, through a new parser::header::tasks_section_body. That returns
the section's line span via pulldown-cmark, so a ## inside a code fence does
not close the section, and H3 headings stay inside it (files group tasks under
### Subsection headings). With no ## Tasks heading at all it appends at end
of file rather than guessing.

The line reported on success now comes from where the task was actually
written, for new files as well, so path:0 is gone.

Fixed in passing

Both in code this PR rewrites:

  • find_end_of_tasks_section and its line-15 estimate are deleted rather than
    ported.
  • insert_into_existing restores the trailing newline that join dropped,
    which had every lash add leaving the file ending mid-line and showing
    \ No newline at end of file in later diffs. This was filed as a minor note
    under the separate task-ID ticket.

Tests

Five integration tests in add_command_test.rs: an empty section lands below
the header and reports the right line, the task is indexed afterwards, a
following ## Notes section survives intact, the new-file path still writes a
well-formed file, and files end in a newline. Plus six unit tests on anchor
resolution covering empty sections, trailing blank lines, a heading inside a
code fence, a file with no Tasks section, and clamping.

Four of the five integration tests were confirmed to fail without the fix.

`lash add` against an existing file whose `## Tasks` section held no
tasks wrote the checkbox at line 1, above the H1. The parser never saw it
as a task, so `lash index` reported 0 tasks and `lash lint` passed clean:
the task was on disk and nowhere else, with exit code 0 and a normal
success message pointing at `path:0`.

`resolve_append` returned `line_number: 0` whenever the file had no
tasks, commented "Signal for new file", and the emitter mapped 0 to
insert index 0. The sentinel conflated "brand-new file" with "existing
file whose Tasks section is empty", and only the first is safe to write
at offset 0.

`PlacementInfo` now carries an `InsertAnchor` instead of a bare line
number: either a concrete `Line(n)` or `EndOfTasksSection`. The ambiguity
cannot recur, because there is no longer a number that means something
other than a line.

Resolving `EndOfTasksSection` needs the source text. A parsed `TaskFile`
records task line numbers and nothing about section boundaries, so an
empty section has nothing to anchor to, which is why the old code fell
back to a hardcoded guess of line 15. The emitter resolves it instead,
against the content it already reads, via a new
`parser::header::tasks_section_body`. That returns the section's line
span using pulldown-cmark, so a `##` inside a code fence does not close
the section, and H3 headings stay inside it (files group tasks under
`### Subsection` headings). With no `## Tasks` heading at all it appends
at end of file rather than guessing.

The line reported on success now comes from where the task was actually
written, for new files as well, so `path:0` is gone.

Two things fixed in passing, both in code this commit rewrites:
`find_end_of_tasks_section` and its line-15 estimate are deleted rather
than ported, and `insert_into_existing` restores the trailing newline
that `join` dropped, which had every `lash add` leaving the file ending
mid-line.

Tests: five integration tests in add_command_test.rs (empty section lands
below the header and reports the right line, is indexed afterwards, a
following `## Notes` section survives intact, the new-file path still
writes a well-formed file, and files end in a newline) plus six unit
tests on anchor resolution covering empty sections, trailing blank lines,
a heading inside a code fence, a file with no Tasks section, and
clamping. Four of the five integration tests fail without the fix.
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.

1 participant