Skip to content

feat: keep cursor at capture insertion when opening file#1117

Closed
chhoumann wants to merge 6 commits intomasterfrom
348-feature-request-keep-cursor-at-position-of-capture-1
Closed

feat: keep cursor at capture insertion when opening file#1117
chhoumann wants to merge 6 commits intomasterfrom
348-feature-request-keep-cursor-at-position-of-capture-1

Conversation

@chhoumann
Copy link
Copy Markdown
Owner

@chhoumann chhoumann commented Feb 23, 2026

Summary

  • keep the cursor at the inserted capture location when a Capture choice writes to a file and then opens it
  • compute insertion position by diffing pre/post file content in a new helper (captureCursor.ts)
  • apply cursor placement for both newly opened tabs and reused existing tabs
  • add regression tests for cursor computation and Capture engine open/reuse flows

Verification

  • bun run test
  • bun run build-with-lint
  • Obsidian CLI (dev vault):
    • reloaded plugin (obsidian vault=dev plugin:reload id=quickadd)
    • executed quickadd:choice:ece95fc5-48b3-49fc-b758-8c9a16b29401
    • confirmed active editor cursor lands at inserted capture line (line 24, ch 0) instead of top-of-file

Notes

  • behavior change is limited to Capture flows that modify file content and open the target file; editor-insertion actions (currentLine / newLineAbove / newLineBelow) are unchanged.

Closes #348

Summary by CodeRabbit

  • New Features

    • Improved cursor placement when capturing and inserting content to existing and new files.
    • Enhanced text formatting with better spacing control when inserting captured content into your notes.
  • Refactor

    • Restructured internal capture workflow for improved reliability and maintainability.
  • Tests

    • Comprehensive test coverage added for cursor positioning, text formatting, and file-handling workflows.

@chhoumann chhoumann linked an issue Feb 23, 2026 that may be closed by this pull request
@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
quickadd Ready Ready Preview Feb 23, 2026 5:43pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements cursor positioning for captured content. It introduces a new cursor mapping module to track insertion boundaries and remap cursor positions across content changes, refactors the capture engine to compute and apply cursor positions, and extends test coverage for the new behavior.

Changes

Cohort / File(s) Summary
Cursor mapping module
src/engine/captureCursor.ts, src/engine/captureCursor.test.ts
New module exporting boundary offset and cursor position mapping functions that track insertion anchors, handle contextual boundary detection, and translate offsets to line/column coordinates; includes unit tests covering boundary remapping and cursor position derivation.
Engine refactoring
src/engine/CaptureChoiceEngine.ts, src/engine/CaptureChoiceEngine.selection.test.ts
Introduces CaptureUpdateResult interface and refactors capture flow into preparation, retrieval, and application phases; adds methods for editor insertion, file opening, and cursor positioning; updates onFileExists and onCreateFileIfItDoesntExist return types; extends test fixtures and adds tests for cursor placement after insertion and recomputation.
Formatter adjustments
src/formatters/captureChoiceFormatter.ts, src/formatters/captureChoiceFormatter-frontmatter.test.ts
Refines insertTextAfterPositionInBody to conditionally insert separator newlines based on insertion context; adds tests verifying line boundary preservation when formatted content lacks trailing newlines.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Engine as CaptureChoiceEngine
    participant Formatter as CaptureChoiceFormatter
    participant Vault as Vault (File System)
    participant Editor as Editor & Leaf

    User->>Engine: Trigger capture with choice
    Engine->>Engine: prepareCaptureRun()
    Engine->>Formatter: Format capture content
    Formatter-->>Engine: Return formatted content + insertionMetadata
    Engine->>Vault: Read existing file (if applicable)
    Vault-->>Engine: Return existing content
    Engine->>Engine: getCaptureUpdateResult()
    Engine->>Engine: Compute captureCursorPosition via boundary mapping
    Engine->>Vault: Modify file with new content
    Vault-->>Engine: Confirm modification
    Engine->>Engine: applyCaptureUpdate()
    Engine->>Editor: openCapturedFileIfNeeded()
    Editor-->>Engine: Return leaf/editor reference
    Engine->>Editor: setLeafCursorIfPossible(cursorPosition)
    Editor-->>Engine: Cursor positioned
    Engine-->>User: Capture complete with cursor at insertion point
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #1017: Integrates cursor-jump and file-opening utilities (jumpToNextTemplaterCursorIfPossible, openExistingFileTab) that are leveraged in the new capture cursor positioning flow.
  • PR #1063: Modifies insertTextAfterPositionInBody newline handling logic in the same formatter file, affecting separator insertion behavior.
  • PR #1055: Updates CaptureChoiceEngine and related test infrastructure with CaptureUpdateResult and capture flow modifications that overlap with this PR's engine refactoring.

Suggested labels

released

Poem

🐰 With whiskers aquiver, the rabbit proclaims:
Now cursors shall dance where the capture was laid,
Through boundaries mapped and offsets remade,
The editor remembers—no more lost in mazes!
Hop forth with precision, through content's new phases! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature: keeping the cursor at the capture insertion location when opening a file.
Linked Issues check ✅ Passed The PR fully implements the feature request from #348: cursor positioning at capture location when file is opened, with proper cursor placement for new and reused tabs.
Out of Scope Changes check ✅ Passed All changes are scoped to capture file-opening flows and cursor positioning logic; editor-insertion actions remain unchanged as specified.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 348-feature-request-keep-cursor-at-position-of-capture-1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Feb 23, 2026

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: a2c40e0
Status:⚡️  Build in progress...

View logs

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/engine/CaptureChoiceEngine.ts (1)

201-230: ⚠️ Potential issue | 🟡 Minor

Potential timing issue between vault.modify and setCursor on a reused tab.

When an existing tab is reused (line 204), the file was just modified via vault.modify (line 182). The editor in the existing tab picks up the change asynchronously through Obsidian's event system. If the editor hasn't fully processed the new content when setCursor fires, the cursor position could land on a stale line count, causing an incorrect placement or a silent failure caught by the try/catch.

The try/catch on lines 220–226 provides a safety net, so this won't crash. But if users report intermittent cursor misplacement on reused tabs, consider adding a small delay or awaiting a vault/workspace event before calling setCursor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/engine/CaptureChoiceEngine.ts` around lines 201 - 230, The reused-tab
path (openExistingFileTab -> openedLeaf) can race with the earlier vault.modify,
so before calling editor.setCursor (when captureCursorPosition is set and editor
exists) wait for the editor to apply the vault changes; implement this by
detecting when openedLeaf was reused and then either awaiting a short tick
(e.g., await new Promise(r => requestAnimationFrame(r)) or a small setTimeout
like 50ms) or subscribing to the workspace/editor update event before calling
editor.setCursor, keeping the try/catch around setCursor and still calling
jumpToNextTemplaterCursorIfPossible afterward.
🧹 Nitpick comments (3)
src/engine/CaptureChoiceEngine.ts (1)

210-218: Verbose but safe type narrowing for the editor.

The inline type assertion to extract editor?.setCursor from the leaf view is pragmatic given the Obsidian API's broad View type. If this pattern appears elsewhere, extracting a small helper (e.g., getEditorFromLeaf(leaf)) could reduce duplication, but it's fine as-is for a single use site.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/engine/CaptureChoiceEngine.ts` around lines 210 - 218, The current inline
type narrowing for extracting editor?.setCursor from openedLeaf?.view is
verbose; extract this logic into a small helper like getEditorFromLeaf(leaf)
that safely narrows the view and returns the editor or undefined, then replace
the inline assertion at the openedLeaf?.view usage with a call to
getEditorFromLeaf(openedLeaf) to reduce duplication and improve readability
while preserving the same safety around optional chaining and setCursor.
src/engine/captureCursor.test.ts (1)

4-28: Tests are correct and cover the primary scenarios well.

I verified each expected result against the algorithm. Consider adding a few more edge-case tests for robustness:

  • CRLF content – ensures toLineAndCh handles \r\n correctly.
  • Only-newline insertion (e.g., "A""A\n") – exercises the fallback to prefixLength.
  • Empty initial content (e.g., """Captured") – covers the "new file" case.

These aren't blocking but would strengthen confidence in the diff logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/engine/captureCursor.test.ts` around lines 4 - 28, Add unit tests for
getCaptureCursorPosition to cover three edge cases: 1) CRLF content — pass
before/after strings using "\r\n" line endings to ensure any internal
toLineAndCh handling of CRLF is correct; 2) only-newline insertion — test a case
like "A" -> "A\n" to exercise the fallback to prefixLength and confirm returned
{line,ch} matches expected; 3) empty initial content — test "" -> "Captured"
(new file) to ensure getCaptureCursorPosition returns the correct insertion
position. Use the existing test structure and assertions to mirror the style of
current cases.
src/engine/captureCursor.ts (1)

28-43: Consider the edge case where nextContent is a strict subset of previousContent (content was removed instead of added).

If the capture formatter unexpectedly removes content (e.g., a merge or format strips text), the suffix-matching loop can produce insertionEnd <= prefixLength, correctly returning null. However, there's a subtle case when content is replaced (some old text removed, some new text added). The function will position the cursor at the start of the replacement region in nextContent, which is reasonable but may not always correspond to a "capture insertion". For the current usage (capture always adds content), this is fine—just noting it for future maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/engine/captureCursor.ts` around lines 28 - 43, The suffix-matching logic
can misinterpret removals/replacements as insertions; add an explicit guard at
the start of the routine (the function that uses previousContent, nextContent,
prefixLength, previousIndex/nextIndex) to bail out when nextContent is shorter
than previousContent (or when a removal is detected) — e.g., if
nextContent.length < previousContent.length return null — so the function only
proceeds for pure-addition cases and avoids positioning the cursor into a
replacement region; keep the existing suffix loop and insertionEnd check but add
this preliminary length/removal check to make intent explicit for future
maintainability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/engine/CaptureChoiceEngine.ts`:
- Around line 201-230: The reused-tab path (openExistingFileTab -> openedLeaf)
can race with the earlier vault.modify, so before calling editor.setCursor (when
captureCursorPosition is set and editor exists) wait for the editor to apply the
vault changes; implement this by detecting when openedLeaf was reused and then
either awaiting a short tick (e.g., await new Promise(r =>
requestAnimationFrame(r)) or a small setTimeout like 50ms) or subscribing to the
workspace/editor update event before calling editor.setCursor, keeping the
try/catch around setCursor and still calling jumpToNextTemplaterCursorIfPossible
afterward.

---

Nitpick comments:
In `@src/engine/CaptureChoiceEngine.ts`:
- Around line 210-218: The current inline type narrowing for extracting
editor?.setCursor from openedLeaf?.view is verbose; extract this logic into a
small helper like getEditorFromLeaf(leaf) that safely narrows the view and
returns the editor or undefined, then replace the inline assertion at the
openedLeaf?.view usage with a call to getEditorFromLeaf(openedLeaf) to reduce
duplication and improve readability while preserving the same safety around
optional chaining and setCursor.

In `@src/engine/captureCursor.test.ts`:
- Around line 4-28: Add unit tests for getCaptureCursorPosition to cover three
edge cases: 1) CRLF content — pass before/after strings using "\r\n" line
endings to ensure any internal toLineAndCh handling of CRLF is correct; 2)
only-newline insertion — test a case like "A" -> "A\n" to exercise the fallback
to prefixLength and confirm returned {line,ch} matches expected; 3) empty
initial content — test "" -> "Captured" (new file) to ensure
getCaptureCursorPosition returns the correct insertion position. Use the
existing test structure and assertions to mirror the style of current cases.

In `@src/engine/captureCursor.ts`:
- Around line 28-43: The suffix-matching logic can misinterpret
removals/replacements as insertions; add an explicit guard at the start of the
routine (the function that uses previousContent, nextContent, prefixLength,
previousIndex/nextIndex) to bail out when nextContent is shorter than
previousContent (or when a removal is detected) — e.g., if nextContent.length <
previousContent.length return null — so the function only proceeds for
pure-addition cases and avoids positioning the cursor into a replacement region;
keep the existing suffix loop and insertionEnd check but add this preliminary
length/removal check to make intent explicit for future maintainability.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 47a7d14 and b8a9d89.

📒 Files selected for processing (4)
  • src/engine/CaptureChoiceEngine.selection.test.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/engine/captureCursor.test.ts
  • src/engine/captureCursor.ts

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/engine/CaptureChoiceEngine.ts (1)

225-253: Consider extracting a named type or using Obsidian's MarkdownView for safer editor access.

The inline type assertion at lines 234–242 works but is verbose and fragile — it will silently produce undefined if the view isn't a markdown view. Since Obsidian exposes MarkdownView with an editor property, you could import it and use an instanceof check for slightly more robust typing:

import { MarkdownView } from "obsidian";
// ...
const editor = openedLeaf?.view instanceof MarkdownView
  ? openedLeaf.view.editor
  : undefined;

This is optional since the current approach is also safe (the typeof editor?.setCursor === "function" guard on line 243 ensures no crash).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/engine/CaptureChoiceEngine.ts` around lines 225 - 253, Replace the
fragile inline union type assertion used to get the editor in
CaptureChoiceEngine with an Obsidian MarkdownView instanceof check: import
MarkdownView from "obsidian", then set editor by testing openedLeaf?.view
instanceof MarkdownView ? openedLeaf.view.editor : undefined (preserving the
existing captureCursorPosition and typeof editor?.setCursor guard and the
surrounding try/catch). This change should be applied where editor is derived
(currently around the openExistingFileTab/openFile handling) and leaves
jumpToNextTemplaterCursorIfPossible and other logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/engine/CaptureChoiceEngine.ts`:
- Around line 225-253: Replace the fragile inline union type assertion used to
get the editor in CaptureChoiceEngine with an Obsidian MarkdownView instanceof
check: import MarkdownView from "obsidian", then set editor by testing
openedLeaf?.view instanceof MarkdownView ? openedLeaf.view.editor : undefined
(preserving the existing captureCursorPosition and typeof editor?.setCursor
guard and the surrounding try/catch). This change should be applied where editor
is derived (currently around the openExistingFileTab/openFile handling) and
leaves jumpToNextTemplaterCursorIfPossible and other logic unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94ce2ff and 7e25a8e.

📒 Files selected for processing (4)
  • src/engine/CaptureChoiceEngine.selection.test.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/engine/captureCursor.test.ts
  • src/engine/captureCursor.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/engine/captureCursor.test.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/formatters/captureChoiceFormatter.ts (1)

17-20: Duplicate CaptureCursorPosition type and helper functions with captureCursor.ts.

CaptureCursorPosition is exported from both this file and src/engine/captureCursor.ts with identical definitions. Similarly, getCursorOffset (line 666) and toLineAndCh (line 689) duplicate the same-named helpers in captureCursor.ts (lines 66–89 and 166–185). Consider importing from captureCursor.ts to keep a single source of truth.

♻️ Suggested approach

Re-export the type from captureCursor.ts and extract the shared helpers there:

-export type CaptureCursorPosition = {
-	line: number;
-	ch: number;
-};
+export type { CaptureCursorPosition } from "../engine/captureCursor";

Then import and delegate to the shared getCursorOffset / toLineAndCh from captureCursor.ts instead of maintaining private copies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/formatters/captureChoiceFormatter.ts` around lines 17 - 20, Duplicate
type and helper implementations exist: CaptureCursorPosition, getCursorOffset,
and toLineAndCh; remove the duplicates in captureChoiceFormatter and
import/re-export the single source from the existing captureCursor module
instead. Replace the local CaptureCursorPosition definition by importing and
re-exporting the type from captureCursor (so other modules still get the type),
and delete the local getCursorOffset and toLineAndCh implementations in
captureChoiceFormatter.ts and call/import the shared getCursorOffset and
toLineAndCh from captureCursor; update any local calls to delegate to those
imported helpers so behavior stays identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/formatters/captureChoiceFormatter.ts`:
- Around line 17-20: Duplicate type and helper implementations exist:
CaptureCursorPosition, getCursorOffset, and toLineAndCh; remove the duplicates
in captureChoiceFormatter and import/re-export the single source from the
existing captureCursor module instead. Replace the local CaptureCursorPosition
definition by importing and re-exporting the type from captureCursor (so other
modules still get the type), and delete the local getCursorOffset and
toLineAndCh implementations in captureChoiceFormatter.ts and call/import the
shared getCursorOffset and toLineAndCh from captureCursor; update any local
calls to delegate to those imported helpers so behavior stays identical.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7e25a8e and e1edc2f.

📒 Files selected for processing (8)
  • src/engine/CaptureChoiceEngine.notice.test.ts
  • src/engine/CaptureChoiceEngine.selection.test.ts
  • src/engine/CaptureChoiceEngine.template-property-types.test.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/engine/captureCursor.test.ts
  • src/engine/captureCursor.ts
  • src/formatters/captureChoiceFormatter.ts
  • src/utilityObsidian.ts

@chhoumann chhoumann force-pushed the 348-feature-request-keep-cursor-at-position-of-capture-1 branch from e1edc2f to 7e25a8e Compare February 23, 2026 15:57
@chhoumann chhoumann closed this Feb 23, 2026
@chhoumann chhoumann deleted the 348-feature-request-keep-cursor-at-position-of-capture-1 branch February 23, 2026 17:43
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.

[FEATURE REQUEST] Keep cursor at position of capture

1 participant