diff --git a/README.md b/README.md index 9989fcd..3299b1f 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,14 @@ There are some default timeouts for long running tasks. Sometimes those timeouts The plugin allows to temporarily disable those timeouts to keep debugging. +## Demo vault + +A demo vault with usage examples ships with every release. You can access it via any of the following: + +1. Running the **Advanced Debug Mode: Open demo vault** command. +2. Downloading `advanced-debug-mode.demo-vault.zip` from the [Releases](https://github.com/mnaoumov/obsidian-advanced-debug-mode/releases). +3. Browsing its source in [`demo-vault/`](./demo-vault/README.md) in this repository. + ## Installation The plugin is available in [the official Community Plugins repository](https://obsidian.md/plugins?id=advanced-debug-mode). diff --git a/demo-vault/.gitignore b/demo-vault/.gitignore new file mode 100644 index 0000000..3b391b5 --- /dev/null +++ b/demo-vault/.gitignore @@ -0,0 +1,13 @@ +# Churny / runtime-generated Obsidian state. +.obsidian/workspace.json +.obsidian/hotkeys.json + +# Built plugins are release-injected by archivePluginDemoVault at release time — +# both the plugin under test AND the obsidian-dev-utils-owned Demo Vault Helper +# bootstrap plugin (which installs, configures, and enables CodeScript Toolkit at +# runtime). Nothing helper-related is committed; commit only the baked settings +# file for the plugin under test. +.obsidian/plugins/* +!.obsidian/plugins/advanced-debug-mode/ +.obsidian/plugins/advanced-debug-mode/* +!.obsidian/plugins/advanced-debug-mode/data.json diff --git a/demo-vault/.markdownlint-cli2.jsonc b/demo-vault/.markdownlint-cli2.jsonc new file mode 100644 index 0000000..bb807df --- /dev/null +++ b/demo-vault/.markdownlint-cli2.jsonc @@ -0,0 +1,12 @@ +{ + // Scoped markdownlint config for the demo vault. The notes are an Obsidian + // vault, so two default rules are inherently incompatible and disabled here; + // every other rule still applies so the notes stay clean. + "config": { + // Notes intentionally open with a `[Docs](...)` link, not a heading. + "MD041": false, + // `[[wikilinks]]` are Obsidian's native link syntax; markdownlint parses + // them as undefined shortcut reference links. + "MD052": false + } +} diff --git a/demo-vault/.obsidian/app.json b/demo-vault/.obsidian/app.json new file mode 100644 index 0000000..e13f1f9 --- /dev/null +++ b/demo-vault/.obsidian/app.json @@ -0,0 +1,8 @@ +{ + "attachmentFolderPath": "_assets/attachments", + "newLinkFormat": "shortest", + "useMarkdownLinks": false, + "showUnsupportedFiles": true, + "defaultViewMode": "source", + "livePreview": true +} diff --git a/demo-vault/.obsidian/community-plugins.json b/demo-vault/.obsidian/community-plugins.json new file mode 100644 index 0000000..534d2dc --- /dev/null +++ b/demo-vault/.obsidian/community-plugins.json @@ -0,0 +1,4 @@ +[ + "demo-vault-helper", + "advanced-debug-mode" +] diff --git a/demo-vault/00 Start.md b/demo-vault/00 Start.md new file mode 100644 index 0000000..ee56d7b --- /dev/null +++ b/demo-vault/00 Start.md @@ -0,0 +1,21 @@ +Welcome to the [Advanced Debug Mode](https://github.com/mnaoumov/obsidian-advanced-debug-mode/) demo vault. This is a plugin for developers: it enriches Obsidian's built-in **debug mode** and the **DevTools console** so that plugin bugs are easier to track down. Nothing here changes how your notes look - the payoff shows up in DevTools, so keep it open (`Ctrl`/`Cmd` + `Shift` + `I` on desktop) as you work through the notes. + +**The headline feature** is _long stack traces_. Normally a JavaScript error dropped through `setTimeout`, `addEventListener`, or a `Promise` chain loses every frame that led up to it, so the console shows a stump of a stack. With this plugin enabled, those async boundaries are stitched back together and you get the full call chain. [[02 Long stack traces]] has a **Run** button that throws such an error so you can see the difference yourself. + +> [!TIP] Interactive buttons +> +> Some notes have **Run** buttons, powered by [`CodeScript Toolkit`](https://github.com/mnaoumov/obsidian-codescript-toolkit/), which this vault installs for you automatically on first open (see [[08 CodeScript Toolkit prerequisite]]). Every button here is honest about what it does - most just throw an error on purpose so you can read the resulting stack trace in DevTools. + +## Features + +- [[01 Debug mode]] +- [[02 Long stack traces]] +- [[03 Async long stack traces]] +- [[04 Long running tasks]] +- [[05 More debug tools]] +- [[06 Settings]] + +## Setup + +- [[07 Code buttons check]] +- [[08 CodeScript Toolkit prerequisite]] diff --git a/demo-vault/01 Debug mode.md b/demo-vault/01 Debug mode.md new file mode 100644 index 0000000..f1ca524 --- /dev/null +++ b/demo-vault/01 Debug mode.md @@ -0,0 +1,23 @@ +[Docs](https://github.com/mnaoumov/obsidian-advanced-debug-mode/) + +# Debug mode + +Obsidian has a built-in **debug mode**. When it is off (the default), Obsidian strips the inline source maps from the plugins it loads, so any error you see in DevTools points at minified, bundled code that is almost impossible to read. Turning debug mode on keeps those source maps, so stack traces point back at the plugin's real source files. + +Advanced Debug Mode gives you a one-click switch for it. + +## How to turn it on + +1. Open the command palette (`Ctrl`/`Cmd` + `P`). +2. Run **Advanced Debug Mode: Open plugin settings** (or open **Settings -> Community plugins -> Advanced Debug Mode**). +3. Toggle **Debug mode** on. + +Obsidian reloads the plugins so the source maps take effect. + +## How to see the difference + +1. Open DevTools (`Ctrl`/`Cmd` + `Shift` + `I`). +2. With debug mode **off**, trigger an error from any plugin (the **Run** button in [[02 Long stack traces]] is a good one) and note that the stack frames point at bundled `main.js` lines. +3. Turn debug mode **on**, trigger the same error, and note that the frames now point at the original `.ts` source files. + +Once debug mode is on, move on to [[02 Long stack traces]] to see the plugin's headline feature. diff --git a/demo-vault/02 Long stack traces.md b/demo-vault/02 Long stack traces.md new file mode 100644 index 0000000..e97c1dd --- /dev/null +++ b/demo-vault/02 Long stack traces.md @@ -0,0 +1,102 @@ +[Docs](https://github.com/mnaoumov/obsidian-advanced-debug-mode/) + +# Long stack traces + +This is the headline feature. When an error is thrown after it has passed through an async boundary - `setTimeout`, `setInterval`, `queueMicrotask`, `requestAnimationFrame`, `addEventListener`, or a `Promise` chain - the JavaScript engine throws away every frame that scheduled the work. The console then shows only the last few frames, which rarely tell you where the problem actually started. + +Advanced Debug Mode stitches those boundaries back together, so the stack trace spans the whole call chain. + +## Try it + +1. Make sure DevTools is open (`Ctrl`/`Cmd` + `Shift` + `I`) and switch to the **Console** tab. +2. Click the **Run** button below. It deliberately throws `Error from foo11` after routing through nine different async hops. +3. Read the stack trace that appears in the console. + +```code-button +--- +caption: Throw an error through async boundaries +--- +function foo1() { + setTimeout(foo2, 100); +} + +function foo2() { + const intervalId = setInterval(foo3, 100); + setTimeout(() => { + clearInterval(intervalId); + }, 150); +} + +function foo3() { + queueMicrotask(foo4); +} + +function foo4() { + requestAnimationFrame(foo5); +} + +function foo5() { + process.nextTick(foo6); +} + +function foo6() { + setImmediate(foo7); +} + +function foo7() { + Promise.resolve().then(foo8); +} + +function foo8() { + Promise.reject(new Error('Error from Promise')).catch(foo9); +} + +function foo9() { + Promise.resolve().finally(foo10); +} + +function foo10() { + const div = createDiv(); + div.addEventListener('click', foo11); + div.click(); +} + +function foo11() { + throw new Error('Error from foo11'); +} + +foo1(); +``` + +## What to look for + +**Without** the plugin the console shows only the tail of the trace: + +```text +Uncaught Error: Error from foo11 + at HTMLDivElement.foo11 () + at foo10 () +``` + +**With** the plugin enabled, each async hop is labelled and the full chain back to `foo1` is preserved: + +```text +Uncaught Error: Error from foo11 + at HTMLDivElement.foo11 () + at foo10 () + at --- addEventListener --- (0) + ... + at --- setTimeout --- (0) + at foo1 () +``` + +## Related settings + +- `shouldIncludeLongStackTraces` turns this feature on or off (on by default). +- `stackTraceLimit` caps how many frames are kept (100 by default) so very deep chains stay readable. +- `shouldIncludeInternalStackFrames` adds Obsidian/Electron-internal frames when you need the whole picture. + +See [[06 Settings]] for all of them, and [[03 Async long stack traces]] for the `async`/`await` variant. + +> [!NOTE] +> `process.nextTick` and `setImmediate` are Node globals, so the button above works on desktop. The rest of the chain works on mobile too. diff --git a/demo-vault/03 Async long stack traces.md b/demo-vault/03 Async long stack traces.md new file mode 100644 index 0000000..233709b --- /dev/null +++ b/demo-vault/03 Async long stack traces.md @@ -0,0 +1,50 @@ +[Docs](https://github.com/mnaoumov/obsidian-advanced-debug-mode/) + +# Async long stack traces + +[[02 Long stack traces]] covers errors routed through callback-style async boundaries. Real code also uses `async`/`await`, and an error thrown inside an awaited function loses the callers that awaited it just the same: + +```js +async function foo() { + await bar(); +} +``` + +Advanced Debug Mode can preserve those `async` frames too, but it is a separate, **opt-in** setting because of the trade-offs below. + +## Turn it on + +1. Open **Settings -> Community plugins -> Advanced Debug Mode**. +2. Toggle **Include async long stack traces** on. This flips the `shouldIncludeAsyncLongStackTraces` setting. + +## Try it + +With DevTools open on the **Console** tab, click the button. It awaits two async hops and then throws. + +```code-button +--- +caption: Throw an error through async/await +--- +async function barAsync1() { + await sleep(50); + await barAsync2(); +} + +async function barAsync2() { + await sleep(50); + throw new Error('Error from barAsync2'); +} + +void barAsync1(); +``` + +With the setting on, the trace keeps the `--- async ---` hops back through `barAsync1`; with it off you see only `barAsync2`. + +> [!WARNING] +> This is the reason it is opt-in: +> +> - Async long stack traces are added **only on desktop** - the mobile JavaScript engine cannot support them. +> - The trace may contain some duplicate frames. +> - While the setting is on, autocompletion in the DevTools console stops working (an Electron bug). + +Turn it off again when you are done to get console autocompletion back. diff --git a/demo-vault/04 Long running tasks.md b/demo-vault/04 Long running tasks.md new file mode 100644 index 0000000..3b0b992 --- /dev/null +++ b/demo-vault/04 Long running tasks.md @@ -0,0 +1,22 @@ +[Docs](https://github.com/mnaoumov/obsidian-advanced-debug-mode/) + +# Long running tasks + +Obsidian guards some internal operations with timeouts: if a task takes too long, Obsidian assumes it has hung and aborts it. That is sensible in normal use, but it fights you while debugging - the moment you pause on a breakpoint and inspect state for a few seconds, the timeout fires and the task you were tracing is torn down before you can finish. + +Advanced Debug Mode lets you relax those timeouts while you debug. + +## The settings involved + +- `shouldTimeoutLongRunningTasks` - leave this **on** for normal use, and turn it **off** before a debugging session so tasks are allowed to run (or pause) indefinitely. +- `shouldIncludeTimedOutTasksDetails` - when a task does time out, include extra details about it in the console so you can see which task was aborted and why. + +Both live in **Settings -> Community plugins -> Advanced Debug Mode** and are documented in [[06 Settings]]. + +## Typical workflow + +1. Open **Settings -> Community plugins -> Advanced Debug Mode** and turn **Timeout long running tasks** off. +2. Set your breakpoints in DevTools and reproduce the slow operation - it will now wait on your breakpoints instead of being aborted. +3. When you are done, turn the setting back on so Obsidian's normal safety timeouts are restored. + +Leaving timeouts disabled outside a debugging session is not recommended - a genuinely stuck task would then hang instead of recovering. diff --git a/demo-vault/05 More debug tools.md b/demo-vault/05 More debug tools.md new file mode 100644 index 0000000..e0dca7e --- /dev/null +++ b/demo-vault/05 More debug tools.md @@ -0,0 +1,23 @@ +[Docs](https://github.com/mnaoumov/obsidian-advanced-debug-mode/) + +# More debug tools + +Two smaller features round out the plugin. Neither has its own setting - they are surfaced through a command and the settings tab. + +## DevTools on mobile + +Obsidian's desktop app has DevTools built in; the mobile app does not, which makes debugging a plugin on a phone or tablet painful. Advanced Debug Mode adds a floating **DevTools** button to the mobile app so you can inspect the console and elements without tethering the device to a desktop. + +- Run **Advanced Debug Mode: Toggle DevTools button** from the command palette to show or hide the button. +- On desktop this is unnecessary (`Ctrl` + `Shift` + `I` already opens DevTools), so the button is aimed at mobile. + +## Debug namespaces + +Many plugins - and `obsidian-dev-utils` itself - use the [`debug`](https://github.com/debug-js/debug) library to emit `console.debug` messages that are hidden unless their namespace is enabled. Advanced Debug Mode adds a UI for managing those namespaces so you can switch specific channels of debug output on and off without editing `localStorage` by hand. + +- Open **Settings -> Community plugins -> Advanced Debug Mode** and find the debug-namespaces section. +- Enable a namespace, reproduce the behavior, and watch the matching `console.debug` output appear in DevTools. + +For the full explanation of debug namespaces, see the [obsidian-dev-utils debugging guide](https://mnaoumov.dev/obsidian-dev-utils/guides/debugging/). + +Head to [[06 Settings]] for the settings-backed features. diff --git a/demo-vault/06 Settings.md b/demo-vault/06 Settings.md new file mode 100644 index 0000000..3e2f40b --- /dev/null +++ b/demo-vault/06 Settings.md @@ -0,0 +1,19 @@ +[Docs](https://github.com/mnaoumov/obsidian-advanced-debug-mode/) + +# Settings + +Open **Settings -> Community plugins -> Advanced Debug Mode** to configure the plugin. Each option below lists the setting key stored in the plugin's `data.json`. + +## Long stack traces + +- `shouldIncludeLongStackTraces` - master switch for the [[02 Long stack traces]] feature (on by default). Turn it off to fall back to the engine's native short traces. +- `stackTraceLimit` - the maximum number of frames kept in a stitched trace (100 by default). Raise it for very deep async chains, lower it to keep the console tidy. +- `shouldIncludeInternalStackFrames` - include Obsidian/Electron-internal frames in the trace. Off keeps traces focused on your code; on shows the complete picture. +- `shouldIncludeAsyncLongStackTraces` - also stitch `async`/`await` boundaries, as described in [[03 Async long stack traces]]. Off by default because it is desktop-only and disables console autocompletion while active. + +## Long running tasks + +- `shouldTimeoutLongRunningTasks` - keep Obsidian's safety timeouts on for normal use, or turn it off before a debugging session so tasks are not aborted while you sit on a breakpoint. See [[04 Long running tasks]]. +- `shouldIncludeTimedOutTasksDetails` - when a task does time out, print extra diagnostic details about it to the console. + +Change any of these and reproduce the matching scenario to watch the behavior update. diff --git a/demo-vault/07 Code buttons check.md b/demo-vault/07 Code buttons check.md new file mode 100644 index 0000000..7e7cdbf --- /dev/null +++ b/demo-vault/07 Code buttons check.md @@ -0,0 +1,13 @@ +[Docs](https://github.com/mnaoumov/obsidian-codescript-toolkit/) + +# Code buttons check + +If [`CodeScript Toolkit`](https://github.com/mnaoumov/obsidian-codescript-toolkit/) is installed and enabled, the block below renders as a **Run** button. Click it - a notice should appear. If you only see a code block, install the prerequisite first: [[08 CodeScript Toolkit prerequisite]]. + +```code-button +--- +caption: Click me +--- +import { Notice } from 'obsidian'; +new Notice('Code buttons work - you are ready to explore the demo vault.'); +``` diff --git a/demo-vault/08 CodeScript Toolkit prerequisite.md b/demo-vault/08 CodeScript Toolkit prerequisite.md new file mode 100644 index 0000000..14e36e2 --- /dev/null +++ b/demo-vault/08 CodeScript Toolkit prerequisite.md @@ -0,0 +1,22 @@ +[Docs](https://github.com/mnaoumov/obsidian-codescript-toolkit/) + +# CodeScript Toolkit prerequisite + +The **Run** buttons in this vault are powered by [`CodeScript Toolkit`](https://github.com/mnaoumov/obsidian-codescript-toolkit/). This vault installs and enables it for you automatically the first time you open the vault (and trust it). Nothing here is required to explore Advanced Debug Mode - the buttons just make it easy to throw a sample error and watch the enriched stack trace in DevTools. + +## If the buttons do not work + +If a code block does not turn into a button, install CodeScript Toolkit manually: + +1. Open **Settings -> Community plugins -> Browse**. +2. Search for **CodeScript Toolkit**, install it, and enable it. +3. Reopen a note that has a **Run** button. + +Or click the button below to install and enable it (needs an internet connection): + +```code-button +--- +caption: Install CodeScript Toolkit +--- +await require('/demoSetup.ts').installAndEnable(app, 'fix-require-modules'); +``` diff --git a/demo-vault/README.md b/demo-vault/README.md new file mode 100644 index 0000000..db4c6a1 --- /dev/null +++ b/demo-vault/README.md @@ -0,0 +1,9 @@ +# Advanced Debug Mode demo vault + +A small Obsidian vault that demonstrates the [Advanced Debug Mode](https://github.com/mnaoumov/obsidian-advanced-debug-mode) plugin - a developer tool that enriches Obsidian's debug mode and the DevTools console so plugin bugs are easier to track down. + +Open [00 Start](<./00 Start.md>) and keep DevTools open (`Ctrl`/`Cmd` + `Shift` + `I`). The star of the vault is [02 Long stack traces](<./02 Long stack traces.md>): its **Run** button throws an error through nine async boundaries so you can watch the plugin stitch the whole call chain back together in the console. + +## First open + +The first time you open this vault, Obsidian treats it as **untrusted**, so the bundled plugins are listed but not loaded until you **Trust author and enable plugins** and reload. After that, the Demo Vault Helper installs [CodeScript Toolkit](https://github.com/mnaoumov/obsidian-codescript-toolkit) (which powers the **Run** buttons in the notes) and opens the start note for you. diff --git a/demo-vault/_assets/CodeScriptToolkit/demoSetup.ts b/demo-vault/_assets/CodeScriptToolkit/demoSetup.ts new file mode 100644 index 0000000..9a0fa32 --- /dev/null +++ b/demo-vault/_assets/CodeScriptToolkit/demoSetup.ts @@ -0,0 +1,17 @@ +import type { App } from 'obsidian'; + +import { Notice } from 'obsidian'; +import { + enableCommunityPlugin, + installCommunityPlugin +} from 'obsidian-dev-utils/obsidian/community-plugins'; + +// Advanced Debug Mode enriches the DevTools console (longer stack traces, debug namespaces, relaxed +// Timeouts), so its interactive demo is a code-button that throws an error and lets you read the enriched +// Stack trace in DevTools - that button's code lives inline in the note. The only helper this vault needs +// Here is the shared CodeScript Toolkit installer used by the prerequisite note's button. +export async function installAndEnable(app: App, pluginId: string): Promise { + await installCommunityPlugin({ app, pluginId }); + await enableCommunityPlugin({ app, pluginId }); + new Notice(`Installed and enabled: ${pluginId}`); +} diff --git a/demo-vault/_assets/CodeScriptToolkit/startup.ts b/demo-vault/_assets/CodeScriptToolkit/startup.ts new file mode 100644 index 0000000..dc13a1c --- /dev/null +++ b/demo-vault/_assets/CodeScriptToolkit/startup.ts @@ -0,0 +1,10 @@ +// Per-vault startup script, run by the universal Demo Vault Helper plugin (via CodeScript +// Toolkit's require) once CodeScript Toolkit is installed and enabled. This is where each +// plugin's demo vault does its own startup setup. Here it just opens the landing note. + +const START_NOTE_PATH = '00 Start.md'; + +const startNote = app.vault.getFileByPath(START_NOTE_PATH); +if (startNote) { + void app.workspace.getLeaf(false).openFile(startNote); +} diff --git a/dprint.json b/dprint.json new file mode 100644 index 0000000..6981058 --- /dev/null +++ b/dprint.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/obsidian-dev-utils/dist/templates/dprint.json", + "excludes": [ + "dist", + "**/node_modules", + "**/*-lock.json", + "**/*.md", + "demo-vault" + ] +} diff --git a/src/demo-vault.no-app.integration.test.ts b/src/demo-vault.no-app.integration.test.ts new file mode 100644 index 0000000..3af4d46 --- /dev/null +++ b/src/demo-vault.no-app.integration.test.ts @@ -0,0 +1,21 @@ +import process from 'node:process'; +import { registerDemoVaultCoverageSuite } from 'obsidian-dev-utils/script-utils/demo-vault-coverage'; +import { getRootFolder } from 'obsidian-dev-utils/script-utils/root'; + +// Keeps the in-repo `demo-vault/` in sync with the plugin's public surface WITHOUT +// Launching Obsidian: it reflects the real config from source and asserts every +// Setting is documented in a note, and that the guard note/member still exist +// (rename drift). Advanced Debug Mode's runtime behavior (long stack traces, debug +// Mode, long-running-task timeouts) is exercised by the desktop/android integration +// Suites, so here only the PluginSettings config class is reflected. +registerDemoVaultCoverageSuite({ + configInterfaces: [{ interfaceName: 'PluginSettings', sourcePath: 'src/plugin-settings.ts' }], + interfaces: [], + nonTrivialGuard: { + expectDemoNote: '06 Settings.md', + expectMember: 'shouldIncludeLongStackTraces', + interfaceName: 'PluginSettings', + sourcePath: 'src/plugin-settings.ts' + }, + rootFolder: getRootFolder() ?? process.cwd() +});