From 4e702570b829acd01e8aee05597579d3bc400228 Mon Sep 17 00:00:00 2001 From: Tobias Davis Date: Tue, 19 Dec 2023 15:01:21 -0600 Subject: [PATCH] change: ignore tasks inside code blocks; update badges better --- .editorconfig | 2 +- .gitignore | 0 .idea/.gitignore | 5 + .idea/jsLibraryMappings.xml | 6 + .idea/markdown.xml | 9 ++ .idea/modules.xml | 8 + .idea/obsidian-gtd-no-next-step.iml | 12 ++ .idea/vcs.xml | 6 + README.md | 12 +- example-vault/.obsidian/app.json | 1 + example-vault/.obsidian/appearance.json | 3 + .../.obsidian/community-plugins.json | 3 + .../.obsidian/core-plugins-migration.json | 30 ++++ example-vault/.obsidian/core-plugins.json | 7 + example-vault/.obsidian/hotkeys.json | 1 + example-vault/.obsidian/templates.json | 3 + example-vault/.obsidian/workspace.json | 86 +++++++++++ example-vault/Hello World.md | 18 +++ example-vault/Projects/Example.md | 1 + example-vault/Templates/Plugin Test.md | 98 ++++++++++++ main.js | 141 ++++++++++++------ manifest.json | 2 +- package.json | 5 +- 23 files changed, 409 insertions(+), 50 deletions(-) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/jsLibraryMappings.xml create mode 100644 .idea/markdown.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/obsidian-gtd-no-next-step.iml create mode 100644 .idea/vcs.xml create mode 100644 example-vault/.obsidian/app.json create mode 100644 example-vault/.obsidian/appearance.json create mode 100644 example-vault/.obsidian/community-plugins.json create mode 100644 example-vault/.obsidian/core-plugins-migration.json create mode 100644 example-vault/.obsidian/core-plugins.json create mode 100644 example-vault/.obsidian/hotkeys.json create mode 100644 example-vault/.obsidian/templates.json create mode 100644 example-vault/.obsidian/workspace.json create mode 100644 example-vault/Hello World.md create mode 100644 example-vault/Projects/Example.md create mode 100644 example-vault/Templates/Plugin Test.md diff --git a/.editorconfig b/.editorconfig index 8aa7fc2..d573a40 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,6 +12,6 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = true -[*.{yml,yaml,toml}] +[*.{yml,yaml,toml,json}] indent_style = space indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 0000000..d23208f --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 0000000..1e34094 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2a0941e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/obsidian-gtd-no-next-step.iml b/.idea/obsidian-gtd-no-next-step.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/obsidian-gtd-no-next-step.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 216680b..478fe26 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ This plugin currently only has the following configurable options: #### Projects folder -The folder where project files live. Default: `Projects/` +The folder where project files live. + +Default: `Projects/` This plugin also works for all sub-folders as well, e.g.: @@ -37,11 +39,15 @@ Projects/ #### Next-Step tag -The tag that indicates a task has a next step. Default: `#next-step` +The tag that indicates a task has a next step. + +Default: `#next-step` #### Waiting-For tag -The tag that indicates a task is waiting for an external action. Default: `#waiting-for` +The tag that indicates a task is waiting for an external action. + +Default: `#waiting-for` ## My GTD Workflow diff --git a/example-vault/.obsidian/app.json b/example-vault/.obsidian/app.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/example-vault/.obsidian/app.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/example-vault/.obsidian/appearance.json b/example-vault/.obsidian/appearance.json new file mode 100644 index 0000000..c8c365d --- /dev/null +++ b/example-vault/.obsidian/appearance.json @@ -0,0 +1,3 @@ +{ + "accentColor": "" +} \ No newline at end of file diff --git a/example-vault/.obsidian/community-plugins.json b/example-vault/.obsidian/community-plugins.json new file mode 100644 index 0000000..1028f88 --- /dev/null +++ b/example-vault/.obsidian/community-plugins.json @@ -0,0 +1,3 @@ +[ + "gtd-no-next-step" +] \ No newline at end of file diff --git a/example-vault/.obsidian/core-plugins-migration.json b/example-vault/.obsidian/core-plugins-migration.json new file mode 100644 index 0000000..e852035 --- /dev/null +++ b/example-vault/.obsidian/core-plugins-migration.json @@ -0,0 +1,30 @@ +{ + "file-explorer": true, + "global-search": false, + "switcher": true, + "graph": false, + "backlink": false, + "canvas": false, + "outgoing-link": false, + "tag-pane": false, + "properties": false, + "page-preview": false, + "daily-notes": false, + "templates": true, + "note-composer": false, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "bookmarks": false, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": false, + "word-count": false, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": false, + "publish": false, + "sync": false +} \ No newline at end of file diff --git a/example-vault/.obsidian/core-plugins.json b/example-vault/.obsidian/core-plugins.json new file mode 100644 index 0000000..37316a0 --- /dev/null +++ b/example-vault/.obsidian/core-plugins.json @@ -0,0 +1,7 @@ +[ + "file-explorer", + "switcher", + "templates", + "command-palette", + "editor-status" +] \ No newline at end of file diff --git a/example-vault/.obsidian/hotkeys.json b/example-vault/.obsidian/hotkeys.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/example-vault/.obsidian/hotkeys.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/example-vault/.obsidian/templates.json b/example-vault/.obsidian/templates.json new file mode 100644 index 0000000..1971e34 --- /dev/null +++ b/example-vault/.obsidian/templates.json @@ -0,0 +1,3 @@ +{ + "folder": "Templates" +} \ No newline at end of file diff --git a/example-vault/.obsidian/workspace.json b/example-vault/.obsidian/workspace.json new file mode 100644 index 0000000..6758881 --- /dev/null +++ b/example-vault/.obsidian/workspace.json @@ -0,0 +1,86 @@ +{ + "main": { + "id": "487bd2982b5b1dce", + "type": "split", + "children": [ + { + "id": "bfa372977a8b84fb", + "type": "tabs", + "children": [ + { + "id": "cfac32c7e03ebb33", + "type": "leaf", + "state": { + "type": "markdown", + "state": { + "file": "Projects/Example.md", + "mode": "source", + "backlinks": false, + "source": false + } + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "ac6138c908bf2ef0", + "type": "split", + "children": [ + { + "id": "902746e70cb575cc", + "type": "tabs", + "children": [ + { + "id": "aeeedd5116b7ef5a", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical" + } + } + }, + { + "id": "22f163d7d3e086f0", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "tag:#waiting-for" + } + } + } + ] + } + ], + "direction": "horizontal", + "width": 300 + }, + "right": { + "id": "ab6ae738fa12851d", + "type": "split", + "children": [], + "direction": "horizontal", + "width": 300, + "collapsed": true + }, + "left-ribbon": { + "hiddenItems": { + "switcher:Open quick switcher": false, + "templates:Insert template": false, + "command-palette:Open command palette": false + } + }, + "active": "cfac32c7e03ebb33", + "lastOpenFiles": [ + "Hello World.md", + "Templates/Plugin Test.md", + "Projects/Example.md", + "test-2023-12-16.md", + "Projects", + "Templates" + ] +} \ No newline at end of file diff --git a/example-vault/Hello World.md b/example-vault/Hello World.md new file mode 100644 index 0000000..bed5e2a --- /dev/null +++ b/example-vault/Hello World.md @@ -0,0 +1,18 @@ + +This vault is primarily used to test this plugin. + +To test it, create a new note from [[Plugin Test]], test each scenario and mark as done and add notes as needed. + +To install the local version of the plugin, you'll need to copy these files: +- `main.js` +- `manifest.json` +- `styles.css` +Into the folder: +- `.obsidian/plugins/gtd-no-next-step` +Tags used: #next-step #waiting-for + +Or, from the root of this repo, simply do: +``` +npm run setup +``` +Then from Obsidian go turn on community plugins, then "View > Force Reload", then got turn this plugin on. diff --git a/example-vault/Projects/Example.md b/example-vault/Projects/Example.md new file mode 100644 index 0000000..3aa8111 --- /dev/null +++ b/example-vault/Projects/Example.md @@ -0,0 +1 @@ +- [ ] this is a task diff --git a/example-vault/Templates/Plugin Test.md b/example-vault/Templates/Plugin Test.md new file mode 100644 index 0000000..ab482b3 --- /dev/null +++ b/example-vault/Templates/Plugin Test.md @@ -0,0 +1,98 @@ + +In lieu of automated tests, here are the things to test when making changes. + +Configuration: +- Folder: `Projects/` +- Next: `#next-step` +- Waiting: `#waiting-for` + +#### 1. File with no tasks +Template: +``` +file with no tasks +``` +Action +- None +Expected outcome: +- [ ] The red badge "No Next" is shown + +#### 2. File with tasks but no next/waiting +Template: +``` +- [ ] this is a task +``` +Action +- None +Expected outcome: +- [ ] The red badge "No Next" is shown + +#### 3. File with only a waiting task +Template: +``` +- [ ] this is a task #waiting-for +``` +Action +- None +Expected outcome: +- [ ] The gray badge "Waiting" is shown + +#### 4. Marking task as done, no tasks left +Template: +``` +- [ ] this is a task #next-step +``` +Action +- Mark the task as done by checking box +Expected outcome: +- [ ] No next step so the red "No Next" badge is shown + +#### 5. No next, marking task as waiting +Template: +``` +- [ ] this is a task +``` +Action +- Add the tag `#waiting-for` to the task +Expected outcome: +- [ ] The gray "Waiting" badge appears + +#### 6. Waiting task, marking as done +Template: +``` +- [ ] this is a task #waiting-for +``` +Action +- Mark the task as done by checking box +Expected outcome: +- [ ] The red "No Next" badge appears + +#### 7. Waiting task, removing tag +Template: +``` +- [ ] this is a task #waiting-for +``` +Action +- Clear the tag from the task +Expected outcome: +- [ ] The red "No Next" badge appears + +#### 8. Moving a project file out +Template: +``` +- [ ] this is a task +``` +Action +- Move out of the `Projects/` folder +Expected outcome: +- [ ] The red "No Next" badge is removed + +#### 9. Moving a file into projects +Create a file outside the `Projects/` folder. +Template: +``` +- [ ] this is a task +``` +Action +- Move into the `Projects/` folder +Expected outcome: +- [ ] The red "No Next" badge is added diff --git a/main.js b/main.js index c78de94..c6a4185 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,4 @@ -const { Notice, Plugin, PluginSettingTab, Setting } = require('obsidian') +const { Plugin, PluginSettingTab, Setting } = require('obsidian') const DEFAULT_SETTINGS = { nextStepTag: '#next-step', @@ -9,31 +9,79 @@ const DEFAULT_SETTINGS = { }, } +function* stringLineIterator(string) { + let cursor = 0 + let newlineIndex = string.indexOf('\n') + while (newlineIndex !== -1) { + yield string.substring(cursor, newlineIndex) + cursor = newlineIndex + 1 + newlineIndex = string.indexOf('\n', cursor) + } + if (cursor < string.length) yield string.substring(cursor) +} + +const CODE_FENCE_CHARS = /^`{3,}/ +function* stringLineIteratorNoCode(string) { + let codeFenceDepth = 0 + for (let line of stringLineIterator(string)) { + let codeFenceCharCount = line.startsWith('```') && CODE_FENCE_CHARS.exec(line)[0].length + if (codeFenceDepth && codeFenceDepth === codeFenceCharCount) { + codeFenceDepth = 0 + } else if (!codeFenceDepth && codeFenceCharCount) { + codeFenceDepth = codeFenceCharCount + } else if (!codeFenceDepth) { + yield line + } + } +} + +const findNextStepOrWaitingFor = (string, nextStepTagRegex, waitingForTagRegex) => { + let hasNextStep = false + for (let line of stringLineIteratorNoCode(string)) { + if (line.includes('- [ ] ')) { + if (waitingForTagRegex.test(line)) return { hasWaitingFor: true } + if (nextStepTagRegex.test(line)) hasNextStep = true + } + } + return { hasNextStep } +} + const makeTaskRegex = tagString => new RegExp(`^\\s*-\\s{1,2}\\[\\s]\\s.*${tagString}[\\W]*`, 'm') -const updateFileBadges = (file, fileItem, isProjectFile) => { - const { nextStep, waitingFor } = file || {} - if (isProjectFile && !nextStep && !waitingFor) { +const clearAllBadges = (fileItem) => { + fileItem.coverEl.removeClass('gtd-no-next-step') + fileItem.coverEl.removeClass('gtd-waiting-for') +} + +const paintFileBadge = (opts, fileItem) => { + const {nextStep, waitingFor} = opts || {} + if (!nextStep && !waitingFor) { + fileItem.coverEl.removeClass('gtd-waiting-for') fileItem.coverEl.addClass('gtd-no-next-step') - } else if (isProjectFile && file.waitingFor) { + } else if (waitingFor) { fileItem.coverEl.removeClass('gtd-no-next-step') fileItem.coverEl.addClass('gtd-waiting-for') } else { - fileItem.coverEl.removeClass('gtd-no-next-step') - fileItem.coverEl.removeClass('gtd-waiting-for') + clearAllBadges(fileItem) } } + module.exports = class GtdNoNextStep extends Plugin { async onload() { await this.loadSettings() - // TODO make lazy this.nextStepTagRegex = makeTaskRegex(this.settings.nextStepTag) this.waitingForTagRegex = makeTaskRegex(this.settings.waitingForTag) - this.registerEvent(this.app.vault.on('delete', async event => { await this.refreshFileBadges(event) })) - this.registerEvent(this.app.vault.on('rename', async event => { await this.refreshFileBadges(event) })) - this.registerEvent(this.app.vault.on('modify', async event => { await this.refreshFileBadges(event) })) + const handleEvent = (event, originalFilename) => { + if (!this.isProjectFile(event.path) && (!originalFilename || !this.isProjectFile(originalFilename))) return + this.updateFileCacheAndMaybeRepaintBadge(event, originalFilename).catch(error => { + console.error('Error while handling event!', error) + }) + } + this.registerEvent(this.app.vault.on('delete', handleEvent)) + this.registerEvent(this.app.vault.on('rename', handleEvent)) + this.registerEvent(this.app.vault.on('modify', handleEvent)) this.app.workspace.onLayoutReady(this.initialize) this.addSettingTab(new SettingTab(this.app, this)) @@ -51,40 +99,44 @@ module.exports = class GtdNoNextStep extends Plugin { && filename.endsWith('.md') && !filename.includes('/_') - containsIncompleteNextStep = string => this.nextStepTagRegex.test(string) - containsIncompleteWaitingFor = string => this.waitingForTagRegex.test(string) + containsIncompleteNextStepOrWaitingFor = string => findNextStepOrWaitingFor(string, this.nextStepTagRegex, this.waitingForTagRegex) - refreshFileBadges = async ({ path, stat, deleted, ...remaining }) => { - const cached = this.settings.projectFileCache[path] || {} - const pathIsProjectFile = this.isProjectFile(path) - let needToSave - let removeBadges - if (!deleted && pathIsProjectFile) { + scheduleRepaintBadge = (path, clearAll) => { + window.setTimeout(() => { + const leaves = this.app.workspace.getLeavesOfType('file-explorer') + if (leaves?.[0]?.view?.fileItems?.[path]) { + if (clearAll) clearAllBadges(leaves[0].view.fileItems[path]) + else paintFileBadge(this.settings.projectFileCache[path], leaves[0].view.fileItems[path]) + } + }) + } + + updateFileCacheAndMaybeRepaintBadge = async ({path, stat, deleted}, originalFilename) => { + if (deleted || !this.isProjectFile(path)) { + delete this.settings.projectFileCache[path] + delete this.settings.projectFileCache[originalFilename] + await this.saveSettings() + return this.scheduleRepaintBadge(path, true) + } + if (!deleted) { const string = await this.app.vault.cachedRead( this.app.vault.getAbstractFileByPath(path) ) - const nextStep = this.containsIncompleteNextStep(string) - if (cached?.nextStep !== nextStep) cached.nextStep = nextStep - const waitingFor = this.containsIncompleteWaitingFor(string) - if (cached?.waitingFor !== waitingFor) cached.waitingFor = waitingFor - cached.mtime = stat.mtime - needToSave = true - } else if (this.settings.projectFileCache[path]) { - delete this.settings.projectFileCache[path] - needToSave = true - } - if (needToSave) { - this.settings.projectFileCache[path] = cached + const {nextStep, waitingFor} = this.settings.projectFileCache[path] || {} + this.settings.projectFileCache[path] = this.settings.projectFileCache[path] || {} + this.settings.projectFileCache[path].mtime = stat.mtime + const { hasNextStep, hasWaitingFor } = this.containsIncompleteNextStepOrWaitingFor(string) + this.settings.projectFileCache[path].nextStep = hasNextStep + this.settings.projectFileCache[path].waitingFor = hasWaitingFor await this.saveSettings() + if ( + this.settings.projectFileCache[path].waitingFor !== waitingFor + || this.settings.projectFileCache[path].nextStep !== nextStep + ) this.scheduleRepaintBadge(path) } - window.setTimeout(() => { - const leaves = this.app.workspace.getLeavesOfType('file-explorer') - if (leaves?.[0]?.view?.fileItems?.[path]) - updateFileBadges(cached, leaves[0].view.fileItems[path], pathIsProjectFile) - }) } - refreshProjectBadges = async () => { + refreshAllFileBadges = async () => { const projectFilesList = this .app .vault @@ -100,12 +152,12 @@ module.exports = class GtdNoNextStep extends Plugin { if (tFile.stat.mtime > (lastCache ? lastCache.mtime : 0)) { needToSave = true const string = await this.app.vault.cachedRead(tFile) - filesMap[tFile.path].nextStep = this.containsIncompleteNextStep(string) - filesMap[tFile.path].waitingFor = this.containsIncompleteWaitingFor(string) + const { hasNextStep, hasWaitingFor } = this.containsIncompleteNextStepOrWaitingFor(string) + filesMap[tFile.path].nextStep = hasNextStep + filesMap[tFile.path].waitingFor = hasWaitingFor } } - for (const path in this.settings.projectFileCache) - if (!filesMap[path]) needToSave = true + for (const path in this.settings.projectFileCache) if (!filesMap[path]) needToSave = true if (needToSave) { this.settings.projectFileCache = filesMap await this.saveSettings() @@ -114,13 +166,13 @@ module.exports = class GtdNoNextStep extends Plugin { if (leaves?.length) { const fileItems = leaves[0].view?.fileItems || {} for (const f in fileItems) if (this.isProjectFile(f)) { - updateFileBadges(filesMap[f], fileItems[f], true) + paintFileBadge(filesMap[f], fileItems[f]) } } } initialize = () => { - this.refreshProjectBadges().catch(error => { + this.refreshAllFileBadges().catch(error => { console.error('Unexpected error in "gtd-no-next-step" plugin initialization.', error) }) } @@ -131,8 +183,9 @@ class SettingTab extends PluginSettingTab { super(app, plugin) this.plugin = plugin } + display() { - const { containerEl } = this + const {containerEl} = this containerEl.empty() new Setting(containerEl) .setName('Projects folder') diff --git a/manifest.json b/manifest.json index 85596ec..5d899ab 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "gtd-no-next-step", "name": "GTD No Next Step", - "version": "1.0.4", + "version": "1.1.0", "description": "Adds a badge to Getting Things Done (GTD) \"project\" files with no defined next step.", "author": "saibotsivad", "authorUrl": "https://davistobias.com", diff --git a/package.json b/package.json index 1bfe447..a2ea333 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,15 @@ { "name": "obsidian-gtd-no-next-step", - "version": "1.0.4", + "version": "1.1.0", "description": "Obsidian plugin that adds a badge to Getting Things Done (GTD) \"project\" files with no defined next step.", "main": "main.js", "repository": { "type": "git", "url": "git+https://github.com/saibotsivad/obsidian-gtd-no-next-step.git" }, + "scripts": { + "setup": "export DIR=example-vault/.obsidian/plugins/gtd-no-next-step && mkdir -p $DIR && cp -f main.js $DIR/main.js && cp -f manifest.json $DIR/manifest.json && cp -f styles.css $DIR/styles.css" + }, "keywords": [ "obsidian", "obsidian-plugin",