fix: Insert targets last active non-generated note, not the passage pane

The action bar / command 'Insert' used the active pane, which is the (generated)
passage note the user just clicked in. Track the last active editable, non-generated
markdown note via active-leaf-change and insert there instead (no focus steal; notifies
which note). Right-click 'Insert here' is unchanged.
This commit is contained in:
Scott Tomaszewski 2026-06-02 22:50:44 -04:00
parent 2ea9bc7f0f
commit 2bc00828a4
3 changed files with 65 additions and 14 deletions

View file

@ -61,10 +61,21 @@ non-obvious things:
- **One selection, globally.** `VerseSelectionService` (plugin-owned, `addChild`) holds a
single active selection + its owning controller. Only the owner renders highlight and the
action bar, so selecting in one passage clears another.
- **The action bar is per-`Document` and `fixed`-positioned.** It's appended to the
passage's own `doc.body` (popout-safe) and positioned from the passage's
`getBoundingClientRect`. Blockquote text is extracted from the owner's `sourceEl` (its
own document) for the same reason — never query the global `document`.
- **The action bar is per-`Document` and bottom-center docked.** It's appended to the
passage's own `doc.body` (popout-safe). It's docked at the bottom-center of the viewport
via CSS — **not** anchored to the passage — because a full chapter is taller than the
viewport, so anchoring to the passage's `getBoundingClientRect().bottom` rendered the bar
off-screen. Blockquote text is extracted from the owner's `sourceEl` (its own document) so
popouts stay correct — never query the global `document`.
- **Selection clears when its passage unloads.** `controller.onunload` calls
`service.clearIfOwner(this)`, so a note re-render or pane close drops a stale selection
and its bar.
- **"Insert" doesn't target the active pane.** When you click a verse, the active pane
*is* the passage's pane — usually a generated note under the Bible content folder, which
is the last place you want the verse inserted. So the plugin tracks the last active
**editable, non-generated** markdown note (`DisciplesJournalPlugin.handleActiveLeafChange`
`lastEditableLeaf`, filtered by `isBibleContentFile`) and `resolveInsertTarget()`
returns it. There's no public "leaves in MRU order" API — this `active-leaf-change`
tracking is the stand-in. Insert does **not** steal focus; it drops the text in and shows
an `Inserted … into <note>` notice. The right-click "Insert here" path is unaffected — it
always uses the clicked editor.

View file

@ -1,4 +1,4 @@
import { MarkdownView, Notice, TFile } from "obsidian";
import { Notice, TFile } from "obsidian";
import { VerseSelection } from "../core/VerseSelection";
import { formatBlockquote, formatCodeBlock, formatInlineReference } from "../utils/VerseFormatter";
import { InsertTargetModal } from "./InsertTargetModal";
@ -63,12 +63,13 @@ export async function runVerseAction(
}
if (kind === "insert") {
const editor = plugin.app.workspace.getActiveViewOfType(MarkdownView)?.editor;
if (!editor) {
new Notice("Open a note and place your cursor to insert.");
const view = plugin.resolveInsertTarget();
if (!view) {
new Notice("No note to insert into — open a note (not a generated Bible note), or right-click where you want it.");
return;
}
editor.replaceSelection(payload);
view.editor.replaceSelection(payload);
new Notice(`Inserted ${selection.label()} into ${view.file?.basename ?? "note"}`);
return;
}

View file

@ -1,4 +1,4 @@
import {Plugin, MarkdownView, Notice, normalizePath, Editor, Menu, MenuItem} from 'obsidian';
import {Plugin, MarkdownView, Notice, normalizePath, Editor, Menu, MenuItem, TFile, WorkspaceLeaf} from 'obsidian';
import {ESVApiService} from '../services/ESVApiService';
import {BibleContentService} from '../services/BibleContentService';
import {BibleReferenceRenderer} from '../components/BibleReferenceRenderer';
@ -37,6 +37,11 @@ export default class DisciplesJournalPlugin extends Plugin {
private bibleMarkupProcessor: BibleMarkupProcessor;
private verseSelectionService: VerseSelectionService;
// The most recently active editable, non-generated markdown note — the target for
// "Insert selected verses" from the action bar/command (the passage's own pane is
// usually a generated Bible note, which we never want to insert into).
private lastEditableLeaf: WorkspaceLeaf | null = null;
async onload() {
// Initialize settings
await this.loadSettings();
@ -104,12 +109,10 @@ export default class DisciplesJournalPlugin extends Plugin {
this.addCommand({
id: 'insert-selected-verses',
name: 'Insert selected verses at cursor',
editorCallback: (editor: Editor) => {
callback: () => {
const active = this.verseSelectionService.get();
if (!active) { new Notice('No verses selected.'); return; }
editor.replaceSelection(
buildPayload(this, active.selection, this.settings.defaultInsertFormat, active.owner.sourceEl)
);
void runVerseAction(this, 'insert', active.selection, this.settings.defaultInsertFormat, active.owner.sourceEl);
}
});
@ -172,6 +175,42 @@ export default class DisciplesJournalPlugin extends Plugin {
handleActiveLeafChange() {
// Refresh theme/styling when active leaf changes
this.updateBibleStyles();
// Remember the last editable, non-generated note so "Insert" targets the note the
// user was working in — not the (generated) passage note they clicked to select.
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view?.file && !this.isBibleContentFile(view.file)) {
this.lastEditableLeaf = view.leaf;
}
}
/** True when a file lives under the configured Bible content folder (a generated note). */
public isBibleContentFile(file: TFile): boolean {
return file.path.startsWith(normalizePath(this.settings.bibleContentVaultPath) + '/');
}
/**
* Resolve where "Insert selected verses" should go: the most recently active editable,
* non-generated markdown note (if still open), else the active note when it isn't a
* generated Bible note. Returns null when there's no suitable target.
*/
public resolveInsertTarget(): MarkdownView | null {
let leaf = this.lastEditableLeaf;
if (leaf) {
let stillOpen = false;
this.app.workspace.iterateRootLeaves((l) => { if (l === leaf) stillOpen = true; });
if (!stillOpen) leaf = null;
}
if (leaf?.view instanceof MarkdownView) {
return leaf.view;
}
const active = this.app.workspace.getActiveViewOfType(MarkdownView);
if (active?.file && !this.isBibleContentFile(active.file)) {
return active;
}
return null;
}
/**