Move the post-audit consistency-fix entries from [Unreleased] under
a dated 1.7.2 heading so the upcoming npm-version bump lands a
CHANGELOG that already reflects the new version.
Two micro-fixes from a post-audit code review:
1. src/main.ts — route addExcludedHeading and addExcludedPhrase catch
blocks through this.log(..., errorMessage(error)) to match the
pattern every other catch in the file already uses. Pre-fix the
diagnostic printed via console.error unconditionally; post-fix it
prints only when the user has debug logging enabled. The user-
facing Notice is unchanged either way.
2. .gitignore — drop *.html and *.zip rules. The *.html rule was
redundant with the docs/ui-mockups/ directory rule that already
catches every HTML file currently in the tree, and the *.zip rule
was a Phase-0-era defense-in-depth concern the CI release pipeline
made obsolete. Removing both means future intentional HTML or zip
files won't be silently swallowed.
CHANGELOG [Unreleased] entry covers both. All 134 vitest tests still
pass; lint and build clean.
Audit is complete (1.7.1 shipped, catalog scan clean), so the planning
docs are now historical reference rather than active roadmap:
- docs/planning/audit-implementation-plan.md -> archive/
- docs/planning/audit-phase-4-tests.md -> archive/
- docs/planning/audit-phase-5-split.md -> archive/
- docs/planning/audit-phase-6-cleanup.md -> archive/
docs/planning/ now contains only the archive/ subdirectory. Future
non-audit planning work can land alongside as needed.
Move the Phase 6 cleanup entry from [Unreleased] under a dated
1.7.1 heading so the upcoming npm-version bump lands a CHANGELOG
that already reflects the new version.
Six cleanup items in one [Unreleased] block: planning-doc archive,
sentence-regex dedup, Canvas polling review + decision, mobile
decision, new architecture overview doc, audit-plan status flip
to complete with intermediate releases footnoted.
The CLAUDE.md rewrite isn't listed because the file is gitignored;
each developer maintains a local copy that didn't reach the
[Unreleased] CHANGELOG via this commit.
audit-implementation-plan.md: the Status checklist ticks all seven
phases as shipped, with the corresponding release tags and dates.
Three intermediate patch releases (1.6.4 / 1.6.5 / 1.6.6) that
shipped during the audit but weren't on the original plan get a
footnote. The closing sentence flips to past tense: the audit is
complete; the doc stays at docs/planning/ as historical reference
until convenient to archive.
audit-phase-6-cleanup.md: Status header flips to Complete; the six
work items each tick with the commit that closed them.
docs/architecture/overview.md describes the post-split module tree,
the data flow for counting a selection, applyExclusions's pipeline
shape, settings persistence (interface + bidirectional runtime /
persisted translation), the override mechanism (frontmatter + inline
markers), status bar + Canvas polling caveat with the event-driven
rationale, build / test / lint commands, the release pipeline, and a
"where to make changes" how-to-modify guide keyed by common
scenarios. Intended as the first stop for navigating the codebase;
the source itself is the second stop.
Closes the Phase 6 "Architecture documentation" item from the audit
plan.
(CLAUDE.md is gitignored, so per-developer copies live outside source
control. The corresponding Phase 6 "do not / prefer" patterns and
post-audit framing live in local CLAUDE.md edits, not in this
commit.)
Two of Phase 6's investigation items reach their conclusions:
- Canvas polling: keep the 500 ms setInterval. The polling exists
because cross-frame selectionchange events don't bubble from
Canvas iframe contentDocuments to the parent document. An
event-driven alternative is technically possible (attach a
selectionchange listener to each iframe contentDocument and
manage that across iframe DOM mutations via MutationObserver)
but the complexity-to-benefit ratio doesn't favor switching.
Current polling is gated by enableLiveCount and no-ops outside
Canvas view.
- Mobile: keep isDesktopOnly: true. Obsidian's developer docs
explicitly state that custom status bar items are not supported
on mobile (addStatusBarItem() is "Not available on mobile"),
and the status bar is substantial in the plugin's UX. Partial
mobile support (gating the status-bar features off on mobile
while keeping the command and modal) is feasible but not
justified at current cost/benefit; revisit if Obsidian adds
mobile status-bar support or if there's specific user interest.
Full reasoning, including the citations and the conditions for a
future revisit, live in docs/planning/audit-phase-6-cleanup.md
Findings.
The abbreviation guard regex had nine duplicate alternation tokens —
'mil', 'museum', 'name', 'pro', 'travel', 'xxx', and 'tel' each
appeared twice; 'xxx' and 'tel' three times. Removed the duplicates,
preserving the first-occurrence order. 53 tokens -> 44 unique.
Behavior-preserving: regex alternations are set-based, so removing
duplicate alternatives doesn't change what the alternation matches.
Note: as Phase 4 finding #2 documented, this guard never actually
fires at runtime because the trailing period is consumed by the
sentence-split regex before the guard's `\\.` requirement can match.
The dedup is a readability fix, not a behavior change. The three
LOCKED-quirk tests in tests/counting/sentences.test.ts that confirm
"Mr.", "Dr.", "etc." produce false splits continue to pass.
Both planning docs cover work that pre-dates the audit and have been
marked complete since well before this audit started. Per the audit
plan's per-phase workflow, completed planning docs move to
docs/planning/archive/ after the work merges. Phase 6 implements that
move for the two longstanding ones; the active audit plan and the
phase planning docs (4, 5, 6) stay at docs/planning/ until the
audit closes.
The archive/ subdirectory is created with these as its first
inhabitants.
Move the Phase 5 split entry from [Unreleased] under a dated 1.7.0
heading so the upcoming npm-version bump lands a CHANGELOG that
already reflects the new version.
CHANGELOG gets the Phase 5 split summary under [Unreleased]: the
17-module decomposition, the applyExclusions consolidation that
removes ~300 lines of duplicated pipeline code, and the no-behavior-
change guarantee that all 134 Phase 4 tests enforce.
Phase 5 planning doc's status flips to Complete and the Findings
section is populated with the pass-by-pass main.ts line counts, the
applyExclusions before/after, the audit-plan §Phase 1 questions that
got resolved during the refactor (URL/path stripping safety net,
DEFAULT_WORD_REGEX dedup), and the final dependency graph.
Pass 6 (final pass) of Phase 5. The CustomSelectedWordCountPlugin class
moves verbatim from top-level main.ts to src/main.ts; top-level main.ts
becomes a thin re-export that bundles the default (for Obsidian) and
the named declarations the characterization test suite imports against
'../../main'.
The esbuild config still points at top-level main.ts as its entry, so
the produced main.js bundle is functionally identical. The plugin id,
manifest, on-disk settings shape, frontmatter property name, and inline
override marker syntax are unchanged.
modal.ts and tab.ts shortened their `import type
CustomSelectedWordCountPlugin from '../../main'` to `from '../main'`
now that the Plugin class is a sibling module rather than reached
through the top-level re-export. Both are still type-only imports;
no runtime cycle is introduced even though src/main.ts in turn imports
WordCountModal and WordCountSettingTab.
Phase 5 results:
- main.ts: 3260 -> 20 lines
- 17 new modules under src/, each focused on one concern
- applyExclusions consolidates the three previously-duplicated count
pipelines into one orchestrator (~150 lines of duplication removed)
- All 134 characterization tests pass without modification at every
commit on this branch
- esbuild bundle remains a single main.js; Obsidian load contract is
unchanged
Phase 5 of the audit plan is complete.
Pass 5 of Phase 5. WordCountSettingTab moves verbatim to its own
module; main.ts imports it where the Plugin class calls
`new WordCountSettingTab(this.app, this)`.
The settings tab is the largest single class in the codebase (~770
lines of UI construction code). Its only domain-level dependency is
DEFAULT_WORD_REGEX, which it uses for the placeholder text and the
"Default" hint in the custom-regex section. The class still
references its Plugin via `import type CustomSelectedWordCountPlugin`,
matching the pattern Pass 4 introduced for the modal.
AppInternals / AppWithInternals moved to src/obsidian-internals.ts
since both the settings tab (log export) and the Plugin class (settings
panel deep-link) need to cast `this.app` through this typed view. Sole
shared dependency on an Obsidian-runtime detail; not large enough to
warrant a folder of its own.
Several obsidian imports trimmed from main.ts (App, ButtonComponent,
DropdownComponent, Platform, PluginSettingTab, Setting, TextComponent,
ToggleComponent) plus DEFAULT_WORD_REGEX — all only used by the
settings tab that just moved out.
main.ts is now 854 lines, down from 3260 at the start of Phase 5
(74% reduction). The remaining content is purely the Plugin entry
class plus its top-of-file imports/re-exports.
All 134 characterization tests pass; lint and build clean.
Pass 4 of Phase 5. Both modal classes move to src/ui/modal.ts; main.ts
imports WordCountModal where the Plugin class instantiates it.
ConfirmModal stays inside the modal module (it's only used by
WordCountModal's "Clear history" button) and is not re-exported.
WordCountModal's plugin parameter is typed as
`CustomSelectedWordCountPlugin` via `import type`, which TypeScript
erases at compile time and therefore introduces no runtime cycle even
while the Plugin class still lives in top-level main.ts. Pass 6 will
move the Plugin class to src/main.ts and the type import path will
shorten correspondingly.
Unused imports trimmed from main.ts (Modal, setIcon, CountResult — all
only referenced from the inline modal class that just moved out).
main.ts is now 1685 lines, down from 1991 at the start of this pass.
All 134 characterization tests pass; lint and build clean.
Pass 3 of Phase 5. The behavioral consolidation: the three count
functions previously each carried an independent copy of the same
exclusion pipeline (~150 lines of duplicated orchestration). Phase 5's
brief calls for collapsing those three copies into one shared function;
this commit does that.
New modules:
- src/counting/pipeline.ts : applyExclusions(text, settings, plugin, disabled)
— the single orchestrator that wraps
processTextWithOverrides around the
seven exclusion processors, gated by
settings + per-call disable list.
- src/counting/characters.ts : countSelectedCharacters
— calls applyExclusions, then runs the
mode switch (all / no-spaces / letters-only).
- src/counting/sentences.ts : countSelectedSentences
— calls applyExclusions, then runs the
sentence-specific code/heading/URL/path
stripping safety net and the regex-based
boundary detection. Phase 4's three
LOCKED-quirk findings are preserved
unchanged.
- src/counting/words.ts : countSelectedWords
— calls applyExclusions, then runs the
word-specific path classification and
extension filter logic. Also picks up
the Phase 1 dedup fix: both regex
branches now construct from
DEFAULT_WORD_REGEX instead of
duplicating the literal inline.
- src/counting/index.ts : countSelectedText aggregator
— calls the three counts in series and
returns { words, characters, sentences }.
The count functions' parameter types changed from
CustomSelectedWordCountPlugin to DebugLoggable, matching the same
boundary that processing helpers crossed in Pass 2.
main.ts is now 1991 lines (down from 3260 at the start of Phase 5,
~39% reduction). The remaining content is the Plugin class, the modal,
and the settings tab — all targeted by later passes.
All 134 characterization tests pass without modification; lint and
build clean. The three LOCKED-quirk tests confirm the consolidation
preserved every pre-existing behavioral oddity.
Pass 2 of Phase 5. Moves the exclusion-pipeline building blocks out of
main.ts into per-concern modules. Each module takes its plugin parameter
as DebugLoggable instead of CustomSelectedWordCountPlugin, so processing
code no longer references main.ts.
Modules created:
- src/processing/frontmatter.ts : stripFrontmatter, getDisabledExclusionsFromFrontmatter
- src/processing/overrides.ts : processTextWithOverrides
- src/processing/code.ts : processCodeBlocks, processInlineCode
- src/processing/comments.ts : processObsidianComments, processHtmlComments
- src/processing/links.ts : processLinks
- src/processing/headings.ts : processHeadings, processSelectiveHeadingSections
- src/processing/words-and-phrases.ts : processWordsAndPhrases
main.ts imports the helpers it still needs internally (count functions
call all seven) and re-exports each so tests/ continues to resolve them
via './main'. The function bodies are byte-for-byte identical to the
pre-split versions; the parameter-type change is type-level only.
main.ts is now 458 lines shorter. All 134 characterization tests pass;
lint and build clean.
Pass 1 of Phase 5. Pulls the lowest-coupling declarations out of main.ts
into the new src/ tree, leaving the file shorter and the new modules
with no internal dependencies.
Moved:
- WordCountPluginSettings, DEFAULT_SETTINGS, DEFAULT_EXCLUSION_LIST,
DEFAULT_WORD_REGEX -> src/settings/types.ts
- CountResult, WordCountHistoryEntry -> src/types.ts
- debugLog, errorMessage -> src/utils/debug.ts
debugLog's parameter is retyped from CustomSelectedWordCountPlugin to a
new DebugLoggable structural interface ({ settings: { enableDebugLogging:
boolean } }). The plugin class satisfies it naturally; processing and
counting modules can now type their optional `plugin?` parameter as
DebugLoggable in later passes and avoid the circular import that would
otherwise result.
main.ts retains:
- The import of these symbols where it consumes them internally
(DEFAULT_SETTINGS in loadSettings, DEFAULT_WORD_REGEX in
countSelectedWords, types throughout).
- Named re-exports of the moved settings types so tests/ and any
external consumer keep working with imports against './main'.
All 134 characterization tests pass; lint and build clean.
Captures the target module layout, extraction order, the
applyExclusions extraction strategy, and the no-behavior-change
constraint that Phase 4's 134-test characterization suite enforces.
Move the Phase 4 vitest entries from [Unreleased] under a dated 1.6.7
heading so the upcoming npm-version bump lands a CHANGELOG that
already reflects the new version.
Vitest test infrastructure with the 134-test characterization suite,
the obsidianmd 0.3.0 bump and the lint-config adaptation it required,
the @types/node bump to align with the Node 22 target, and the dead
.eslintignore cleanup. All Phase 4 work is internal: no user-visible
behavior changes.
Phase 4 added a vitest characterization suite. Hook it into the release
workflow so a failing test blocks the draft release alongside lint and
build. Ordering matches the per-phase quality gates: Lint, Test, Build.
134 tests across six files lock the current behavior of the three count
entry points and their support code, giving Phase 5's planned single-file
split a load-bearing safety net.
Files:
- vitest.config.ts: node environment; resolve.extensions prefers .ts so
imports resolve to main.ts source (not the main.js build artifact);
alias redirects 'obsidian' to the test mock.
- tests/mocks/obsidian.ts: minimal no-op stubs (App, Plugin, Modal,
Setting, etc.) so main.ts's imports resolve in tests.
- tests/fixtures/settings.ts: makeSettings(overrides) helper for spreading
DEFAULT_SETTINGS with per-test overrides.
- tests/counting/words.test.ts (37 tests): baseline, code/comment/link/
heading/words-and-phrases/extension/path exclusion modes, advanced
regex, combination cases.
- tests/counting/characters.test.ts (18 tests): all/no-spaces/letters-only
modes plus the exclusion matrix.
- tests/counting/sentences.test.ts (21 tests): baseline, abbreviation /
decimal / URL / file-path / file-extension / ellipsis guards, and
settings-driven exclusions.
- tests/counting/text.test.ts (5 tests): countSelectedText aggregator.
- tests/exclusions/overrides.test.ts (18 tests): the four override
mechanisms (frontmatter array, string, "all" expansion, inline
comment markers in both HTML and Obsidian styles) plus the
disabledExclusions parameter.
- tests/exclusions/processors.test.ts (35 tests): direct unit tests for
the individual process* helpers.
Three pre-existing quirks surfaced during test-writing and are locked
in by 'LOCKED quirk:' tests so a later phase can decide whether to fix
each:
- Path-exclusion buffer is greedy (a detected path swallows trailing
words to end of input).
- Sentence-detection abbreviation guard never fires (the period is
consumed by the split regex before the guard runs).
- Sentence-detection file-extension guard discards entire sentences
ending in name.ext rather than just the in-filename period.
Details in docs/planning/audit-phase-4-tests.md § Findings.
Phase 4's characterization test suite needs to import the counting
entry points and the processing helpers. Add 'export' to the relevant
top-level declarations:
- stripFrontmatter, getDisabledExclusionsFromFrontmatter
- processTextWithOverrides
- processCodeBlocks, processInlineCode
- processObsidianComments, processHtmlComments
- processLinks
- processHeadings, processSelectiveHeadingSections
- processWordsAndPhrases
- countSelectedCharacters, countSelectedSentences
- countSelectedText, countSelectedWords
- WordCountPluginSettings interface
- CountResult interface
- DEFAULT_EXCLUSION_LIST, DEFAULT_SETTINGS
No runtime behavior change; the export keyword is a no-op at the
bundler level. Phase 5's planned module split was going to make
these visible at the file boundary anyway.
The 0.3.0 recommended config has one entry that applies its rules to
every file matched by 'eslint .' without a files: restriction. Some
of those rules (e.g. no-plugin-as-component) require typescript-eslint
parser services and crash on non-TS inputs like package.json and
eslint.config.mjs. Wrap each unrestricted entry to scope it to
**/*.ts and **/*.tsx.
Also extend the ignores array with common non-source file types
(.json, .md, .css, .yml, .lock, .nvmrc, etc.) so an 'eslint .'
invocation can't inadvertently feed them through any future
unrestricted rule.
Removed .eslintignore — ESLint 9 no longer reads it (its presence
triggers ESLintIgnoreWarning on every run), and both entries
(node_modules/, main.js) were already in eslint.config.mjs's
ignores array.
- @types/node was pinned to ^16.11.6 while the project's actual Node
target (.nvmrc) is 22.20.0. The mismatch blocked installing vitest,
whose peer dependency requires @types/node@^18 || ^20 || >=22. Types
are dev-only and erased at runtime.
- Added vitest@^3 to host the Phase 4 characterization test suite.
- Bumped eslint-plugin-obsidianmd from ^0.2.9 to ^0.3.0 to match the
version the Obsidian Community website's automated rescan runs
server-side, so local lint stays aligned with what the catalog
scanner sees on release tags.
The fix(types) commit landed after the initial CHANGELOG entry was
cut, so the 1.6.6 section was missing an honest accounting of the
type-narrowing work that went in to clear the new CI lint gate. Add
one Internal bullet describing the errorMessage helper, the catch-
block migration, the frontmatter input narrowing, and the loadData
return cast.
The 1.6.6-rc1 CI run failed at the lint step on ten
`@typescript-eslint/no-unsafe-*` findings - the family I had
previously dismissed as "local strictness, not catalog barriers."
That was true for the community-site rescan, but CI runs
`npm run lint` strictly and these errors block the workflow.
Seven of the ten findings are `error.message` / `e.message` reads in
`catch` blocks where TypeScript's useUnknownInCatchVariables types
the binding as `unknown`. Add a module-level `errorMessage(error:
unknown): string` helper that narrows via `instanceof Error`, then
route each catch-block message access through it. Falls back to
`String(error)` for thrown primitives or non-Error objects.
The remaining three:
- `getDisabledExclusionsFromFrontmatter` reads `cache.frontmatter
['cswc-disable']` (typed `any` from the Obsidian metadata cache),
assigning it to a local. Annotate the local as `unknown` so the
subsequent `Array.isArray` / `typeof` checks are real narrowings.
The `.filter(item => ...)` for the array branch uses a `(item):
item is string =>` type predicate so the filtered result is
properly typed `string[]` instead of `any[]`.
- `loadSettings` was assigning the result of `Object.assign({},
DEFAULT_SETTINGS, await this.loadData())` to the typed
`this.settings`. `Plugin.loadData()` returns `Promise<any>`, so
the merged result was `any`. Cast `loadData()`'s return through
`as Partial<WordCountPluginSettings> | null` to make the merge
well-typed.
Lint now exits clean (zero errors) which clears the CI gate.
Capture the two threads landing in this release: the responsive
@media block now uses `:has()` for selector scoping and drops the
four `!important` declarations (CSS rescan win), and the new
GitHub Actions release workflow produces verifiable artifact
attestations for every release asset.
The `@media (max-width: 600px)` block targeted the bare `.modal`
class - every Obsidian modal, not just the plugin's own - and used
`!important` four times to override Obsidian's default modal width
on narrow viewports. Change the selector to
`.modal:has(.word-count-modal)`, which scopes the rule to this
plugin's modal and supplies enough specificity to drop all four
`!important` declarations.
Trades the four `!important` rescan warnings for one `:has()`
warning (net -3). The replacement matches the `:has()` pattern
already used at `styles.css:87` and elsewhere; both are
catalog-acceptance Warnings, not Errors, so this is a code-quality
improvement rather than a release blocker.
The non-`.modal` selectors in the same rule list
(`.word-count-label`, `.word-count-count`, etc.) are already
plugin-specific; they did not need `!important` for specificity in
the first place and now drop it along with the `.modal` rewrite.
Adopt the same release pipeline that landed in Draft Bench, adapted
to this plugin's existing scripts and assets.
What lands together:
- `.github/workflows/release.yml`. Tag-push trigger matching plain
SemVer (`1.6.6`) and pre-release SemVer (`1.6.6-rc1`). Runs
`npm ci`, `npm run lint` (the obsidianmd ruleset), and
`npm run build`. Calls `actions/attest-build-provenance@v2` on
`main.js`, `manifest.json`, and `styles.css` so each release asset
carries a verifiable GitHub artifact attestation. Creates a draft
release with the three assets attached; pre-release tags get
`--prerelease`. The draft step preserves the editorial gate -
release-description markdown is pasted into the draft by hand
before publishing, keeping writing-style review on every release.
- `.nvmrc` pinning Node 22.20.0 (matches local dev). Both CI and
local environments resolve the same Node via
`actions/setup-node@v4`'s `node-version-file: '.nvmrc'`.
- `docs/developer/release.md` rewritten to document the CI flow:
required artifacts, the trial-run procedure with `-rc1` tags, the
attestation-verification command, and the recovery path for a
failed CI run.
Customizations from the reference workflow:
- Dropped the `lint:css` step. The plugin has CSS sources but no
stylelint config; adding one is real follow-up work and the
community-site rescan covers CSS lint server-side until that
lands.
- Dropped the `test` step. No test infrastructure yet; that's Phase
4 of the audit plan. The step is straightforward to add later.
Release assets are the standard Obsidian three: `main.js` (built
fresh by CI), `manifest.json` (committed), `styles.css` (committed).
Documents the duplicate-selector merges and the two redundant
`!important` removals from the previous commit. The four `!important`
uses in the `@media (max-width: 600px)` block are intentionally left
in place since the alternative (`:has()` or JS-driven classes) would
trade one rescan warning for another. The remaining six `:has()`
selectors are similarly load-bearing and not addressed here.
Three pairs of duplicate selectors were doing partial styling against
the same element. Merge each pair into one rule with the cascaded
final values:
- `.word-count-modal .modal-header` at lines 104 + 174. The earlier
"Original ... (restored)" block was a subset of the "Enhanced"
block that came after; the cascade was already using the second
block's values. Drop the duplicate and keep the merged result.
- `.word-count-modal .modal-title` at lines 109 + 191. The two
blocks split between flex layout (109) and final font sizing (191).
Combine into a single rule with `display: flex`, `align-items`,
`gap` from the first and `font-size: 18px` from the second.
- `.word-count-settings-group` at lines 667 + 852 (both inside the
`.word-count-settings` parent). Two separate `&:has(...)` nested
rules collapse into one parent block with both nested
`&:has(...)` selectors as siblings.
Drop `!important` from two utility rules where it was redundant:
- `.word-count-hidden { display: none !important; }` -> no
`!important`. The class is toggled via `toggleClass` on settings
sub-section containers; nothing sets a competing inline `display`
on those elements, so the class-based rule wins on cascade.
- `body.word-count-hide-core .status-bar-item.plugin-word-count {
display: none !important; }` -> no `!important`. The selector
already has high specificity (body class plus two element-class
selectors), and Obsidian's core word-count item carries no
competing rule.
The four `!important` uses inside the `@media (max-width: 600px)`
block stay. They override Obsidian's default `.modal` width, which
needs either a higher-specificity selector or `:has()` to replace.
The latter would just trade one rescan warning for another, so
deferring until a cleaner replacement is available.
Captures the three buckets addressed by this patch: the CSS brace
balance error (the only Error in the community-site rescan), the
timer-function migration from `activeWindow.*` back to `window.*`
(eight sites, mirrors the rescan's "Timer functions should use
'window'" guidance), and the nine no-explicit-any cleanups
(logging helpers, `getHeadingAtCursor`, and the
`(this.app as any).setting / appVersion` escape hatches).
Artifact attestations for `main.js` / `styles.css` remain
Recommendations only and are deferred to Phase 6 of the audit plan,
which adds an actual release workflow with
`actions/attest-build-provenance`.
The community-site rescan flagged nine `any` uses left after the
1.6.3 Phase 3e settings-callback typing pass. Each kind addressed:
- The two logging helpers (`debugLog` at module scope, `log` as a
Plugin private method) take a rest-args `...args: any[]`. Both
forward args to `console.log` (which accepts `unknown[]`), so
`unknown[]` is the right swap. No call-site changes needed.
- `getHeadingAtCursor(editor: any, cursor: any)` was already called
from the `editor-menu` workspace event, which provides the
arguments as `Editor` and `EditorPosition` (both from `obsidian`).
Importing those types and typing the parameters removes both
`any`s. The function body only uses `editor.getLine(...)` and
`cursor.line`, both available on the proper types.
- The five `(this.app as any).setting.open()` /
`openTabById(...)` / `appVersion` accesses are reaching for
internal App members that aren't in Obsidian's published types.
Declare a local `AppInternals` interface that names the members
we use, plus `type AppWithInternals = App & AppInternals`, and
cast through that. Still a cast, but no longer `any` - the rule
fires on `any` specifically, not on intersection-typed assertions.
The 1.6.3 Phase 3b migration switched bare `setTimeout` /
`clearTimeout` / `setInterval` / `clearInterval` calls to
`activeWindow.*` per `eslint-plugin-obsidianmd@0.2.9`'s
`prefer-active-window-timers` rule. The community-site rescan
disagreed: timer functions should use `window.*`, not `activeWindow.*`.
Eight sites in main.ts (debounce timer, canvas polling, Reading-view
Select-All, two copy-button feedback flashes, the onunload
debounce-clear) now use `window.setTimeout` / `clearTimeout` /
`setInterval` / `clearInterval`.
`window.*` is more predictable than `activeWindow.*` for timer ids:
`activeWindow` is a focus-following getter, so a timer id obtained
from `activeWindow.setTimeout()` could end up tied to a different
window than the corresponding `activeWindow.clearTimeout()` if focus
shifted in between. `window.*` always refers to the same window
object that holds the plugin's runtime context, which is the right
semantic for the debounce / polling / animation timers here.
`prefer-active-window-timers` does not flag the explicit `window.*`
form, so the local lint config doesn't need adjustment.
styles.css had an extra `}` on line 679 that prematurely closed the
`.word-count-container-indented` rule, leaving the second indentation
rule and everything after (down to line 882) accidentally outside
the `.word-count-settings` container. The `}` at line 883 was then
unmatched, which the community-site CSS lint flagged as an Error.
The fix removes the stray closer and lets the rule nest naturally:
`.word-count-container-indented` opens once, contains its own
padding rule, then nests a deeper-indentation child rule before the
parent closes. Total brace count is now balanced (141 open / 141
close).
Move the accumulated [Unreleased] entries (Phase 0 release-blocker
work, Phase 2 bug fixes plus cleanup, Phase 3 API conformance sweep,
the popout listener fix, and the supporting documentation changes)
under a dated `[1.6.3] - 2026-05-12` heading. Leave [Unreleased]
empty for the next cycle.
Adds a Fixed entry describing the listener-binding bug surfaced by
the popout smoke test, the per-window registration via
workspace.on('window-open'), the activeWindow.getSelection() change,
and the known limitation that the plugin's status-bar item still
lives in the main window only.
The Phase 3b migration of `document` -> `activeDocument` was
incomplete for cross-window event listeners. `activeDocument` is a
getter that resolves to the currently-focused document at the moment
its line evaluates - at `setupStatusBar()` time, that's almost always
the main window. The listeners ended up bound to the main document
only, so live status bar updates never fired from selections inside a
popped-out leaf.
Three changes:
1. Move listener registration out of `setupStatusBar()` (where it was
gated on `enableLiveCount` and ran in whichever document was
active at the time) into `onload()`. Use `this.registerDomEvent`
on the main window's document at load, and subscribe to
`workspace.on('window-open', ...)` so each popout that opens later
gets the same two listeners on its own document. `handleSelectionChange`
and `handleKeyDown` already bail when `enableLiveCount` is off, so
permanent registration is safe.
2. Replace every `window.getSelection()` with `activeWindow.getSelection()`
(six sites across the selection / Ctrl+A / Canvas paths). The
handler now reads selection from whichever window currently has
focus, so selections inside a popout produce the right text.
3. Drop the manual `removeEventListener` calls from `setupStatusBar`
(settings-change re-entry) and `onunload`. `registerDomEvent` /
`registerEvent` clean up on plugin unload automatically; the
handler's `enableLiveCount` guard handles the settings-change case
without needing to detach.
Canvas polling stays in `setupStatusBar` since it's tied to the
status bar item's lifecycle, not to whichever document is focused.
Known limitation surfaced by this change: the plugin's status-bar
item still lives in the main window only (Obsidian's
`addStatusBarItem` adds to the main status bar). Selections inside a
popout now correctly update that main-window item, but the popout's
own status bar does not carry the plugin's `Selected: N` text.
Expand the obsidianmd/ui/sentence-case brand list to include Unix,
Windows, macOS, Linux, iOS, and Android so the setting names read
naturally ("Exclude Windows paths", "Exclude Unix paths", "(e.g.,
C:\)", "Exclude Unix-style paths..."). Markdown and Obsidian were
already in the list.
The catch: the same lint rule then wants to capitalize the brand
inside the lowercase identifier strings used by cswc-disable
overrides (`exclude-windows-paths` -> `exclude-Windows-paths`),
which would break the runtime lookup if a user copy-pasted the
capitalized identifier into their note's frontmatter. Each path
description carrying that identifier gets a targeted
`eslint-disable-next-line` so the brand stays correctly capitalized
in prose but the identifier stays lowercase as the parser expects.