diff --git a/main.ts b/main.ts index 2419fcb..a613f60 100644 --- a/main.ts +++ b/main.ts @@ -1,12 +1,12 @@ -import { App, Notice, Plugin, PluginSettingTab, Setting, parseYaml } from "obsidian"; +import { App, Notice, Plugin, PluginSettingTab, Setting, parseYaml, moment } from "obsidian"; import { RangeSetBuilder } from "@codemirror/state"; import { - Decoration, - DecorationSet, - EditorView, - ViewPlugin, - ViewUpdate, - WidgetType, + Decoration, + DecorationSet, + EditorView, + ViewPlugin, + ViewUpdate, + WidgetType, } from "@codemirror/view"; /* ========================= @@ -16,79 +16,120 @@ import { interface DateCalcSettings { debug: boolean; hideResultWhileCursorInside: boolean; - verbose: boolean; + verbose: boolean; } const DEFAULT_SETTINGS: DateCalcSettings = { debug: false, hideResultWhileCursorInside: true, - verbose: false, // default to concise + verbose: false, }; /* ========================= - Helpers + Moment helpers ========================= */ -function zeroTime(d: Date): Date { - const nd = new Date(d); - nd.setHours(0, 0, 0, 0); - return nd; -} - -function parseDate(input: any): Date | null { - if (!input) return null; - const d = new Date(input); - if (isNaN(d.getTime())) return null; - return d; -} - -function humanizeDuration(ms: number): string { - const abs = Math.abs(ms); - let seconds = Math.floor(abs / 1000); - let minutes = Math.floor(seconds / 60); - let hours = Math.floor(minutes / 60); - let days = Math.floor(hours / 24); - const years = Math.floor(days / 365); - days = days % 365; - const months = Math.floor(days / 30); - days = days % 30; - hours = hours % 24; - minutes = minutes % 60; - seconds = seconds % 60; - - const parts: string[] = []; - if (years) parts.push(`${years} year${years !== 1 ? "s" : ""}`); - if (months) parts.push(`${months} month${months !== 1 ? "s" : ""}`); - if (days) parts.push(`${days} day${days !== 1 ? "s" : ""}`); - if (hours && parts.length < 3) parts.push(`${hours} hour${hours !== 1 ? "s" : ""}`); - if (minutes && parts.length < 3) parts.push(`${minutes} minute${minutes !== 1 ? "s" : ""}`); - if (seconds && parts.length < 3) parts.push(`${seconds} second${seconds !== 1 ? "s" : ""}`); - - if (!parts.length) return "0 seconds"; - return parts.join(", "); -} +type M = moment.Moment; function isLivePreviewEditor(cm: EditorView): boolean { - // In Obsidian, the editor is inside .markdown-source-view - // Live Preview adds .is-live-preview on that container. const container = cm.dom.closest(".markdown-source-view"); return !!container?.classList.contains("is-live-preview"); } +/** + * Parse various inputs into a Moment. + * - "YYYY-MM-DD" => local startOf("day") + * - YAML Date objects that represent UTC midnight => keep same calendar date locally + * - Other date-time inputs => moment(...) as-is + */ +export function parseMoment(input: unknown): M | null { + if (input == null || input === "") return null; + + // YAML can produce Date objects (often representing UTC midnight for date-only scalars) + if (input instanceof Date) { + if (Number.isNaN(input.getTime())) return null; + + const isUtcMidnight = + input.getUTCHours() === 0 && + input.getUTCMinutes() === 0 && + input.getUTCSeconds() === 0 && + input.getUTCMilliseconds() === 0; + + if (isUtcMidnight) { + return moment({ + year: input.getUTCFullYear(), + month: input.getUTCMonth(), + day: input.getUTCDate(), + }).startOf("day"); + } + + return moment(input); + } + + const s = String(input).trim(); + + // Strict date-only => local midnight + const dateOnly = moment(s, "YYYY-MM-DD", true); + if (dateOnly.isValid()) return dateOnly.startOf("day"); + + // Fallback: let Moment interpret other formats (ISO with time/offset, etc.) + const any = moment(s); + return any.isValid() ? any : null; +} + +function formatNiceDate(m: M): string { + return m.format("MMMM Do, YYYY"); +} + +/** + * Calendar-aware breakdown (years, months, days, hours) with remainder behavior. + * Returns a formatted string (verbose or concise). + */ +function formatSpan(from: M, to: M, verbose: boolean): { text: string; isNegative: boolean } { + let a = from.clone(); + let b = to.clone(); + + const isNegative = b.isBefore(a); + if (isNegative) [a, b] = [b, a]; + + const years = b.diff(a, "years"); a.add(years, "years"); + const months = b.diff(a, "months"); a.add(months, "months"); + const days = b.diff(a, "days"); a.add(days, "days"); + const hours = b.diff(a, "hours"); + + if (verbose) { + const parts: string[] = []; + if (years) parts.push(`${years} year${years === 1 ? "" : "s"}`); + if (months) parts.push(`${months} month${months === 1 ? "" : "s"}`); + if (days) parts.push(`${days} day${days === 1 ? "" : "s"}`); + if (hours && parts.length < 3) parts.push(`${hours} hour${hours === 1 ? "" : "s"}`); + return { text: parts.join(", ") || "0 days", isNegative }; + } else { + const parts: string[] = []; + if (years) parts.push(`${years}y`); + if (months) parts.push(`${months}mo`); + if (days) parts.push(`${days}d`); + if (hours && parts.length < 3) parts.push(`${hours}h`); + return { text: parts.join(" ") || "0d", isNegative }; + } +} /* ========================= - Inline processor + Inline args parsing ========================= */ function parseKv(params: string): Record { const cfg: Record = {}; - // key=value where value can be "..." or '...' or a bare token const rx = /(\w+)=("([^"\\]|\\.)*"|'([^'\\]|\\.)*'|\S+)/g; + let m: RegExpExecArray | null; while ((m = rx.exec(params)) !== null) { const key = m[1]; let val = m[2]; - if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { + if ( + (val.startsWith('"') && val.endsWith('"')) || + (val.startsWith("'") && val.endsWith("'")) + ) { val = val.slice(1, -1); } cfg[key] = val; @@ -97,125 +138,166 @@ function parseKv(params: string): Record { } function parseInlineArgs(params: string): any { - // 1) If it looks like key=value syntax, parse that first (fast + reliable) + if (!params) return {}; + + // Prefer key=value (fast, predictable) if (params.includes("=")) return parseKv(params); - // 2) Otherwise try YAML (for `{type: diff, from: ..., to: ...}` and similar) + // Otherwise attempt YAML (supports `{ type: diff, from: ..., to: ... }`) try { const v = parseYaml(params); - // If YAML returns a string, just return it (supports `date-calc: birthday`) return v ?? {}; } catch { - // Last resort: empty - return {}; + // Fall back to raw string type alias like: `date-calc: birthday` + return params.trim(); } } +function normalizeType(paramsRaw: string, cfg: any): { type: string; cfg: any } { + const params = paramsRaw.trim(); + let type = (cfg?.type ?? "").toString().toLowerCase().trim(); + // `date-calc: birthday` + if (!type && typeof cfg === "string" && /^[A-Za-z][A-Za-z-]*$/.test(cfg.trim())) { + type = cfg.trim().toLowerCase(); + cfg = {}; + } + if (!type && /^[A-Za-z][A-Za-z-]*$/.test(params)) { + type = params.toLowerCase(); + cfg = {}; + } + + // Infer from fields + if (!type && cfg && typeof cfg === "object") { + if (cfg.birthday || cfg.birthdate) type = "birthday"; + else if (cfg.to || cfg.until) type = "countdown"; + else if (cfg.since) type = "since"; + else if (cfg.from && cfg.to) type = "diff"; + } + + // Aliases + if (type === "bday") type = "birthday"; + if (type === "until") type = "countdown"; + if (type === "difference") type = "diff"; + + return { type, cfg }; +} + +/* ========================= + Inline processor +========================= */ // raw should be WITHOUT backticks: "date-calc: birthday=1992-08-16" -function processInlineDateCalc(raw: string, app: App, sourcePath: string): string { - if (!/^date-calc\s*:/.test(raw)) return ""; +function processInlineDateCalc( + raw: string, + app: App, + sourcePath: string, + settings: DateCalcSettings +): { text: string; tooltip?: string } { + if (!/^date-calc\s*:/.test(raw)) return { text: "" }; - const params = raw.replace(/^date-calc\s*:/, "").trim(); - let cfg: any = params ? parseInlineArgs(params) : {}; + const paramsRaw = raw.replace(/^date-calc\s*:/, "").trim(); + let cfg: any = parseInlineArgs(paramsRaw); + const norm = normalizeType(paramsRaw, cfg); + const type = norm.type; + cfg = norm.cfg; - // Support `date-calc: birthday` - let type = (cfg?.type ?? "").toString().toLowerCase().trim(); - if (!type && typeof cfg === "string" && /^[A-Za-z][A-Za-z-]*$/.test(cfg.trim())) { - type = cfg.trim().toLowerCase(); - cfg = {}; - } - if (!type && /^[A-Za-z][A-Za-z-]*$/.test(params)) type = params.toLowerCase(); + if (!type) return { text: "" }; - // Infer - if (!type) { - if (cfg.birthday || cfg.birthdate) type = "birthday"; - else if (cfg.to || cfg.until) type = "countdown"; - else if (cfg.since) type = "since"; - else if (cfg.from && cfg.to) type = "diff"; - } + const verbose = settings.verbose; - // Aliases - if (type === "bday") type = "birthday"; - if (type === "until") type = "countdown"; - if (type === "difference") type = "diff"; - if (!type) return ""; + try { + switch (type) { + case "birthday": { + const fileCache = app.metadataCache.getCache(sourcePath); + const fm = (fileCache?.frontmatter ?? {}) as any; - try { - switch (type) { - case "birthday": { - const fileCache = app.metadataCache.getCache(sourcePath); - const fm = (fileCache?.frontmatter ?? {}) as any; - const bstr = cfg.birthday || cfg.birthdate || cfg.date || fm?.birthday || fm?.birthdate; - const birthDate = parseDate(bstr); - if (!birthDate) return `date-calc: Missing or invalid "birthday" date.`; + const bstr = + cfg.birthday || + cfg.birthdate || + cfg.date || + fm?.birthday || + fm?.birthdate; - const today = zeroTime(new Date()); - const bd = zeroTime(birthDate); + const bd = parseMoment(bstr)?.startOf("day"); + if (!bd) return { text: `date-calc: Missing or invalid "birthday" date.` }; - let age = today.getFullYear() - bd.getFullYear(); - const m = today.getMonth() - bd.getMonth(); - if (m < 0 || (m === 0 && today.getDate() < bd.getDate())) age--; + const today = moment().startOf("day"); - const nextBirthday = zeroTime(new Date(today.getFullYear(), bd.getMonth(), bd.getDate())); - if (today > nextBirthday) nextBirthday.setFullYear(today.getFullYear() + 1); + const age = today.diff(bd, "years"); - const oneDay = 24 * 60 * 60 * 1000; - const isBirthdayToday = today.getMonth() === bd.getMonth() && today.getDate() === bd.getDate(); - const daysUntil = isBirthdayToday ? 0 : Math.round((nextBirthday.getTime() - today.getTime()) / oneDay); + const next = bd.clone().year(today.year()); + if (next.isBefore(today, "day")) next.add(1, "year"); - let msg = ""; - if (daysUntil > 31) { - let monthsUntil = - (nextBirthday.getFullYear() - today.getFullYear()) * 12 + - (nextBirthday.getMonth() - today.getMonth()); - if (today.getDate() > nextBirthday.getDate()) monthsUntil--; - msg = `Next birthday in ${monthsUntil} months.`; - } else if (daysUntil === 0) msg = "Wish them Happy Birthday!"; - else if (daysUntil === 1) msg = "Their birthday is tomorrow!"; - else msg = `Next birthday in ${daysUntil} days.`; + const daysUntil = next.diff(today, "days"); - return `Age: ${age} years. ${msg}`; - } + let msg: string; + if (daysUntil === 0) msg = verbose ? "Wish them Happy Birthday!" : "Birthday!"; + else if (daysUntil === 1) msg = verbose ? "Their birthday is tomorrow!" : "Tomorrow!"; + else if (daysUntil > 31) { + const monthsUntil = next.diff(today, "months"); + msg = verbose ? `Next birthday in ${monthsUntil} months.` : `Next in ${monthsUntil}mo`; + } else { + msg = verbose ? `Next birthday in ${daysUntil} days.` : `Next in ${daysUntil}d`; + } - case "countdown": { - const toStr = cfg.to || cfg.date || cfg.until; - const toDate = parseDate(toStr); - if (!toDate) return `date-calc: Missing or invalid "to" date.`; + const ageStr = verbose ? `${age} years` : `${age}y`; + return { + text: `Age: ${ageStr}. ${msg}`, + tooltip: formatNiceDate(bd), + }; + } - const fromDate = parseDate(cfg.from) || new Date(); - const diffMs = toDate.getTime() - fromDate.getTime(); - const label = cfg.label ? `${cfg.label}: ` : ""; + case "countdown": { + const to = parseMoment(cfg.to || cfg.date || cfg.until); + if (!to) return { text: `date-calc: Missing or invalid "to" date.` }; - return diffMs >= 0 - ? `${label}Countdown: ${humanizeDuration(diffMs)}` - : `${label}Event passed ${humanizeDuration(-diffMs)} ago`; - } + const from = parseMoment(cfg.from) ?? moment(); + const label = cfg.label ? `${cfg.label}: ` : ""; - case "diff": { - const fromD = parseDate(cfg.from || cfg.start); - const toD = parseDate(cfg.to || cfg.end); - if (!fromD || !toD) return `date-calc: Provide valid "from" and "to" dates.`; + const span = formatSpan(from, to, verbose); - const diffMs = toD.getTime() - fromD.getTime(); - return `Difference: ${humanizeDuration(Math.abs(diffMs))}${diffMs < 0 ? " (to is before from)" : ""}`; - } + const text = !span.isNegative + ? `${label}${verbose ? "Countdown: " : ""}${span.text}` + : `${label}${verbose ? "Event passed " : ""}${span.text}${verbose ? " ago" : ""}`; - case "since": { - const sinceDate = parseDate(cfg.since || cfg.from || cfg.date); - if (!sinceDate) return `date-calc: Missing or invalid "since" date.`; + return { text }; + } - const diffMs = Date.now() - sinceDate.getTime(); - return diffMs >= 0 ? `Since: ${humanizeDuration(diffMs)} ago` : `In: ${humanizeDuration(-diffMs)}`; - } + case "diff": { + const from = parseMoment(cfg.from || cfg.start); + const to = parseMoment(cfg.to || cfg.end); + if (!from || !to) return { text: `date-calc: Provide valid "from" and "to" dates.` }; - default: - return "date-calc: Unknown type. Supported: birthday, countdown, diff, since"; - } - } catch (e: any) { - return `date-calc error: ${e?.message || e}`; - } + const span = formatSpan(from, to, verbose); + + const text = verbose + ? `Difference: ${span.text}${span.isNegative ? " (to is before from)" : ""}` + : `Diff: ${span.text}${span.isNegative ? " (reverse)" : ""}`; + + return { text }; + } + + case "since": { + const since = parseMoment(cfg.since || cfg.from || cfg.date); + if (!since) return { text: `date-calc: Missing or invalid "since" date.` }; + + const now = moment(); + const span = formatSpan(since, now, verbose); + + const text = !span.isNegative + ? `Since: ${span.text}${verbose ? " ago" : ""}` + : `In: ${span.text}`; + + return { text }; + } + + default: + return { text: "date-calc: Unknown type. Supported: birthday, countdown, diff, since" }; + } + } catch (e: any) { + return { text: `date-calc error: ${e?.message || e}` }; + } } /* ========================= @@ -223,22 +305,40 @@ function processInlineDateCalc(raw: string, app: App, sourcePath: string): strin ========================= */ class DateCalcWidget extends WidgetType { - constructor(private text: string) { super(); } - eq(other: DateCalcWidget) { return other.text === this.text; } + constructor( + private text: string, + private tooltip: string | undefined, + private from: number, + private to: number + ) { super(); } + + eq(other: DateCalcWidget) { + return other.text === this.text && other.tooltip === this.tooltip; + } toDOM() { const span = document.createElement("span"); span.className = "date-calc-inline"; span.textContent = this.text; span.setAttribute("contenteditable", "false"); + + // store source range for click-to-edit + span.dataset.dcFrom = String(this.from); + span.dataset.dcTo = String(this.to); + + if (this.tooltip) { + span.className += " has-tooltip"; + span.setAttribute("aria-label", this.tooltip); + } return span; } + + ignoreEvent() { return false; } // important for interaction [web:64] } -export function dateCalcLivePreview(app: any, getSettings: () => { debug: boolean }) { - // Match only inline-code expressions (backticked) +export function dateCalcLivePreview(app: App, getSettings: () => DateCalcSettings) { const re = /`date-calc:[^`]*`/g; - // Key fix: scan beyond visibleRanges so matches don't get missed at viewport boundaries + // Scan past viewport boundaries so matches don't flicker/miss near edges const margin = 2000; return ViewPlugin.fromClass( @@ -256,18 +356,19 @@ export function dateCalcLivePreview(app: any, getSettings: () => { debug: boolea } build(view: EditorView): DecorationSet { - // IMPORTANT: do nothing in Source mode so raw markdown stays visible - if (!isLivePreviewEditor(view)) return Decoration.none; + // Do nothing in Source mode so raw markdown stays visible + if (!isLivePreviewEditor(view)) return Decoration.none; - const b = new RangeSetBuilder(); + const settings = getSettings(); const sourcePath = app.workspace.getActiveFile()?.path ?? ""; const head = view.state.selection.main.head; + const b = new RangeSetBuilder(); + const used = new Set(); + let matchesSeen = 0; let replaced = 0; - const used = new Set(); - for (const r of view.visibleRanges) { const scanFrom = Math.max(0, r.from - margin); const scanTo = Math.min(view.state.doc.length, r.to + margin); @@ -282,46 +383,67 @@ export function dateCalcLivePreview(app: any, getSettings: () => { debug: boolea const absStart = scanFrom + m.index; const absEnd = absStart + m[0].length; - // de-dupe overlaps from overlapping scan windows + // De-dupe overlaps from overlapping scan windows const key = `${absStart}:${absEnd}`; if (used.has(key)) continue; used.add(key); - // If cursor is inside, don't replace (so user can edit the raw inline code) - if (head >= absStart && head <= absEnd) continue; + // Optional: keep raw inline code visible while editing it + const sel = view.state.selection.main; + const intersects = sel.from <= absEnd && sel.to >= absStart; + + if (settings.hideResultWhileCursorInside && intersects) continue; + const inner = m[0].slice(1, -1).trim(); // remove backticks - const result = processInlineDateCalc(inner, app, sourcePath); - if (!result) continue; + const result = processInlineDateCalc(inner, app, sourcePath, settings); + if (!result.text) continue; - // Replace the entire backticked expression with a widget b.add( absStart, absEnd, Decoration.replace({ - widget: new DateCalcWidget(result), - inclusive: false, - }) + widget: new DateCalcWidget(result.text, result.tooltip, absStart, absEnd), + inclusive: false, + }) ); replaced++; } } - if (getSettings().debug) { + if (settings.debug) { console.log("[date-calc] replace scan", { matchesSeen, replaced, sourcePath }); } return b.finish(); } }, - { - decorations: (v) => v.decorations, + { + decorations: (v) => v.decorations, - // Important for replace widgets: treat them as “atomic” so cursor navigation behaves sanely - // (same pattern as CodeMirror’s decoration examples) - provide: (plugin) => - EditorView.atomicRanges.of((view) => view.plugin(plugin)?.decorations ?? Decoration.none), - } + eventHandlers: { + mousedown: (e, view) => { + const el = (e.target as HTMLElement | null)?.closest?.(".date-calc-inline") as HTMLElement | null; + if (!el) return false; + + const from = Number(el.dataset.dcFrom); + const to = Number(el.dataset.dcTo); + if (!Number.isFinite(from) || !Number.isFinite(to)) return false; + + // Select the whole backticked expression so it immediately “reveals” for editing + view.dispatch({ + selection: { anchor: from, head: to }, + scrollIntoView: true, + }); + view.focus(); + + return true; // CodeMirror will preventDefault when handler returns true [web:64] + }, + }, + + provide: (plugin) => + EditorView.atomicRanges.of((view) => view.plugin(plugin)?.decorations ?? Decoration.none), + } ); } @@ -330,31 +452,38 @@ export function dateCalcLivePreview(app: any, getSettings: () => { debug: boolea ========================= */ export default class DateCalcPlugin extends Plugin { - settings: DateCalcSettings; + settings: DateCalcSettings; - async onload() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + async onload() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - console.log("[date-calc] loaded"); - new Notice("Date Calc loaded (Live Preview)"); + if (this.settings.debug) console.log("[date-calc] loaded"); + new Notice("Date Calc loaded (Live Preview)"); - // Live Preview requires editor extensions + decorations - this.registerEditorExtension(dateCalcLivePreview(this.app, () => this.settings)); // [page:2][page:1] - + this.registerEditorExtension(dateCalcLivePreview(this.app, () => this.settings)); - this.addCommand({ - id: "date-calc-toggle-debug", - name: "Date Calc: Toggle debug", - callback: async () => { - this.settings.debug = !this.settings.debug; - await this.saveData(this.settings); - new Notice(`Date Calc debug: ${this.settings.debug ? "ON" : "OFF"}`); - console.log("[date-calc] debug =", this.settings.debug); - }, - }); + this.addCommand({ + id: "date-calc-toggle-debug", + name: "Date Calc: Toggle debug", + callback: async () => { + this.settings.debug = !this.settings.debug; + await this.saveData(this.settings); + new Notice(`Date Calc debug: ${this.settings.debug ? "ON" : "OFF"}`); + }, + }); - this.addSettingTab(new DateCalcSettingTab(this.app, this)); - } + this.addCommand({ + id: "date-calc-toggle-verbose", + name: "Date Calc: Toggle verbose output", + callback: async () => { + this.settings.verbose = !this.settings.verbose; + await this.saveData(this.settings); + new Notice(`Date Calc verbose: ${this.settings.verbose ? "ON" : "OFF"}`); + }, + }); + + this.addSettingTab(new DateCalcSettingTab(this.app, this)); + } } /* ========================= @@ -362,47 +491,46 @@ export default class DateCalcPlugin extends Plugin { ========================= */ class DateCalcSettingTab extends PluginSettingTab { - constructor(app: App, private plugin: DateCalcPlugin) { - super(app, plugin); - } + constructor(app: App, private plugin: DateCalcPlugin) { + super(app, plugin); + } - display(): void { - const { containerEl } = this; - containerEl.empty(); + display(): void { + const { containerEl } = this; + containerEl.empty(); - containerEl.createEl("h2", { text: "Date Calc" }); + containerEl.createEl("h2", { text: "Date Calc" }); - new Setting(containerEl) - .setName("Debug logging") - .setDesc("Logs how many `date-calc:` inline-code matches were seen and decorated.") - .addToggle((t) => - t.setValue(this.plugin.settings.debug).onChange(async (v) => { - this.plugin.settings.debug = v; - await this.plugin.saveData(this.plugin.settings); - }) - ); + new Setting(containerEl) + .setName("Debug logging") + .setDesc("Logs how many `date-calc:` inline-code matches were seen and decorated.") + .addToggle((t) => + t.setValue(this.plugin.settings.debug).onChange(async (v) => { + this.plugin.settings.debug = v; + await this.plugin.saveData(this.plugin.settings); + }) + ); - new Setting(containerEl) - .setName("Hide result while cursor inside inline code") - .setDesc("Prevents the widget from showing while you edit the inline backticks.") - .addToggle((t) => - t.setValue(this.plugin.settings.hideResultWhileCursorInside).onChange(async (v) => { - this.plugin.settings.hideResultWhileCursorInside = v; - await this.plugin.saveData(this.plugin.settings); - }) - ); + new Setting(containerEl) + .setName("Hide result while cursor inside inline code") + .setDesc("Prevents the widget from showing while you edit the inline backticks.") + .addToggle((t) => + t.setValue(this.plugin.settings.hideResultWhileCursorInside).onChange(async (v) => { + this.plugin.settings.hideResultWhileCursorInside = v; + await this.plugin.saveData(this.plugin.settings); + }) + ); - new Setting(containerEl) - .setName("Verbose output") - .setDesc("When enabled, show the longer/wordier messages (your current output).") - .addToggle(t => - t.setValue(this.plugin.settings.verbose).onChange(async (v) => { - this.plugin.settings.verbose = v; - await this.plugin.saveData(this.plugin.settings); - }) - ); + new Setting(containerEl) + .setName("Verbose output") + .setDesc("When enabled, show longer/wordier messages.") + .addToggle((t) => + t.setValue(this.plugin.settings.verbose).onChange(async (v) => { + this.plugin.settings.verbose = v; + await this.plugin.saveData(this.plugin.settings); + }) + ); - - containerEl.createEl("p", { text: 'Example: `date-calc: birthday=1992-08-16`' }); - } + containerEl.createEl("p", { text: 'Example: `date-calc: birthday=1992-08-16`' }); + } }