Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude subtasks, add option to manually refresh from command palette #19

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.js]
ij_javascript_spaces_within_imports = true
ij_javascript_space_before_function_left_parenth = false
ij_javascript_spaces_within_object_literal_braces = true


[{*.ats, *.ts}]
ij_typescript_spaces_within_imports = true
ij_typescript_space_before_function_left_parenth = false
ij_typescript_spaces_within_object_literal_braces = true
34 changes: 19 additions & 15 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
# OS X
.DS_Store

# Intellij
*.iml
.idea

# npm / yarn
node_modules
package-lock.json
yarn-error.log

# build
main.js
*.js.map
# macOS
.DS_Store

# IntelliJ
*.iml
.idea

# npm / yarn
node_modules
package-lock.json
yarn-error.log
yarn.lock

# build
main.js
*.js.map

# my settings
data.json
File renamed without changes.
File renamed without changes.
56 changes: 0 additions & 56 deletions model/TodoParser.test.ts

This file was deleted.

10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "obsidian-sample-plugin",
"version": "0.9.7",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"name": "obsidian-plugin-todo",
"version": "0.1.0",
"description": "Text-based GTD in Obsidian.",
"main": "main.js",
"scripts": {
"build": "rollup --config rollup.config.js",
Expand All @@ -10,8 +10,8 @@
"test": "jest"
},
"keywords": [],
"author": "",
"license": "MIT",
"author": "https://github.com/larslockefeer",
"license": "GPL-3.0-or-later",
"devDependencies": {
"@rollup/plugin-commonjs": "^15.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
Expand Down
4 changes: 2 additions & 2 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
input: 'main.ts',
input: 'src/main.ts',
output: {
dir: '.',
dir: './build',
sourcemap: 'inline',
format: 'cjs',
exports: 'default',
Expand Down
File renamed without changes.
29 changes: 26 additions & 3 deletions main.ts → src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,31 @@ import { VIEW_TYPE_TODO } from './constants';
import { TodoItemView, TodoItemViewProps } from './ui/TodoItemView';
import { TodoItem, TodoItemStatus } from './model/TodoItem';
import { TodoIndex } from './model/TodoIndex';
import { DEFAULT_SETTINGS, TodoPluginSettings, TodoPluginSettingTab } from './settings'

export default class TodoPlugin extends Plugin {
settings: TodoPluginSettings;

private todoIndex: TodoIndex;
private view: TodoItemView;

constructor(app: App, manifest: PluginManifest) {
super(app, manifest);
this.todoIndex = new TodoIndex(this.app.vault, this.tick.bind(this));
this.todoIndex = new TodoIndex(this.app.vault, this.tick.bind(this), this);
}

async onload(): Promise<void> {
await this.loadSettings();
this.addSettingTab(new TodoPluginSettingTab(this.app, this));

this.addCommand({
id: 'refresh-all',
name: 'Refresh All',
callback: () => {
this.prepareIndex(true);
},
});

this.registerView(VIEW_TYPE_TODO, (leaf: WorkspaceLeaf) => {
const todos: TodoItem[] = [];
const props = {
Expand Down Expand Up @@ -52,8 +66,8 @@ export default class TodoPlugin extends Plugin {
});
}

async prepareIndex(): Promise<void> {
await this.todoIndex.initialize();
async prepareIndex(notify: boolean = false): Promise<void> {
await this.todoIndex.initialize(notify);
}

tick(todos: TodoItem[]): void {
Expand All @@ -64,4 +78,13 @@ export default class TodoPlugin extends Plugin {
};
});
}

async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}

async saveSettings() {
await this.saveData(this.settings);
await this.prepareIndex();
}
}
21 changes: 12 additions & 9 deletions model/TodoIndex.ts → src/model/TodoIndex.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { TAbstractFile, TFile, Vault } from 'obsidian';
import { Notice, TAbstractFile, TFile, Vault } from 'obsidian';
import { TodoItem, TodoItemStatus } from '../model/TodoItem';
import { TodoParser } from '../model/TodoParser';
import TodoPlugin from '../main'

export class TodoIndex {
private vault: Vault;
private todos: Map<string, TodoItem[]>;
private listeners: ((todos: TodoItem[]) => void)[];
private plugin: TodoPlugin;

constructor(vault: Vault, listener: (todos: TodoItem[]) => void) {
constructor(vault: Vault, listener: (todos: TodoItem[]) => void, plugin: TodoPlugin) {
this.vault = vault;
this.todos = new Map<string, TodoItem[]>();
this.listeners = [listener];
this.plugin = plugin;
}

async initialize(): Promise<void> {
async initialize(notify: boolean = false): Promise<void> {
// TODO: persist index & last sync timestamp; only parse files that changed since then.
const todoMap = new Map<string, TodoItem[]>();
let numberOfTodos = 0;
Expand All @@ -29,11 +32,11 @@ export class TodoIndex {
}

const totalTimeMs = new Date().getTime() - timeStart;
console.log(
`[obsidian-plugin-todo] Parsed ${numberOfTodos} TODOs from ${markdownFiles.length} markdown files in (${
totalTimeMs / 1000.0
}s)`,
);
const msg = `Parsed ${numberOfTodos} TODO${numberOfTodos > 1 ? 's' : ''} from ${markdownFiles.length} note${markdownFiles.length > 1 ? 's' : ''}`;
console.log('[obsidian-plugin-todo] ' + msg + ` in (${totalTimeMs / 1000.0}s)`);
if (notify) {
new Notice(msg);
}
this.todos = todoMap;
this.registerEventHandlers();
this.invokeListeners();
Expand Down Expand Up @@ -72,7 +75,7 @@ export class TodoIndex {

private async parseTodosInFile(file: TFile): Promise<TodoItem[]> {
// TODO: Does it make sense to index completed TODOs at all?
const todoParser = new TodoParser();
const todoParser = new TodoParser(this.plugin);
const fileContents = await this.vault.cachedRead(file);
return todoParser
.parseTasks(file.path, fileContents)
Expand Down
File renamed without changes.
70 changes: 70 additions & 0 deletions src/model/TodoParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { TodoItemStatus } from './TodoItem';
import { TodoParser } from './TodoParser';

const todoParser = new TodoParser();

test('parsing an outstanding todo', async () => {
const contents = `- [ ] This is something that needs doing`;
const todos = await todoParser.parseTasks('/', contents);
const todo = todos[0];
expect(todo.startIndex).toEqual(2);
expect(todo.length).toEqual(38);
expect(todo.sourceFilePath).toEqual('/');
expect(todo.status).toEqual(TodoItemStatus.Todo);
expect(todo.description).toEqual('This is something that needs doing');
expect(todo.actionDate).toBeUndefined();
expect(todo.isSomedayMaybeNote).toEqual(false);
});

test('parsing a completed todo', async () => {
const contents = `- [x] This is something that has been completed`;
const todos = await todoParser.parseTasks('/', contents);
const todo = todos[0];
expect(todo.startIndex).toEqual(2);
expect(todo.length).toEqual(45);
expect(todo.sourceFilePath).toEqual('/');
expect(todo.status).toEqual(TodoItemStatus.Done);
expect(todo.description).toEqual('This is something that has been completed');
expect(todo.actionDate).toBeUndefined();
expect(todo.isSomedayMaybeNote).toEqual(false);
});

test('parsing an outstanding todo with a specific action date', async () => {
const contents = `- [ ] This is something that needs doing #2021-02-16`;
const todos = await todoParser.parseTasks('/', contents);
const todo = todos[0];
expect(todo.startIndex).toEqual(2);
expect(todo.length).toEqual(50);
expect(todo.sourceFilePath).toEqual('/');
expect(todo.status).toEqual(TodoItemStatus.Todo);
expect(todo.description).toEqual('This is something that needs doing #2021-02-16');
expect(todo.actionDate).toEqual(new Date('2021-02-16'));
expect(todo.isSomedayMaybeNote).toEqual(false);
});

test('parsing an outstanding someday/maybe todo', async () => {
const contents = `- [ ] This is something that needs doing #someday`;
const todos = await todoParser.parseTasks('/', contents);
const todo = todos[0];
expect(todo.startIndex).toEqual(2);
expect(todo.length).toEqual(47);
expect(todo.sourceFilePath).toEqual('/');
expect(todo.status).toEqual(TodoItemStatus.Todo);
expect(todo.description).toEqual('This is something that needs doing #someday');
expect(todo.actionDate).toBeUndefined();
expect(todo.isSomedayMaybeNote).toEqual(true);
});

test('parsing nested todos', async () => {
const contents = `- [ ] This is a root task\n - [ ] And this a subtask\n- [x] And another root`;
const todos = await todoParser.parseTasks('/', contents);
expect(todos.length).toEqual(2);
const todo = todos[1];
expect(todo.startIndex).toEqual(55);
expect(todo.length).toEqual(20);
expect(todo.sourceFilePath).toEqual('/');
expect(todo.status).toEqual(TodoItemStatus.Done);
expect(todo.description).toEqual('And another root');
expect(todo.actionDate).toBeUndefined();
expect(todo.isSomedayMaybeNote).toEqual(false);
});
16 changes: 13 additions & 3 deletions model/TodoParser.ts → src/model/TodoParser.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { TodoItem, TodoItemStatus } from '../model/TodoItem';
import TodoPlugin from "../main";

export class TodoParser {
async parseTasks(filePath: string, fileContents: string): Promise<TodoItem[]> {
const pattern = /(-|\*) \[(\s|x)?\]\s(.*)/g;
private plugin: TodoPlugin;

constructor (plugin: TodoPlugin) {
this.plugin = plugin;
}

async parseTasks (filePath: string, fileContents: string): Promise<TodoItem[]> {
let pattern = /([-*]) \[([ x])\] (.*)/gm;
if (this.plugin.settings.onlyRootTasks) {
pattern = /^([-*]) \[([ x])\] (.*)$/gm;
}
return [...fileContents.matchAll(pattern)].map((task) => this.parseTask(filePath, task));
}

private parseTask(filePath: string, entry: RegExpMatchArray): TodoItem {
private parseTask (filePath: string, entry: RegExpMatchArray): TodoItem {
const todoItemOffset = 2; // Strip off `-|* `
const status = entry[2] === 'x' ? TodoItemStatus.Done : TodoItemStatus.Todo;
const description = entry[3];
Expand Down
44 changes: 44 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import TodoPlugin from "./main";

export const DEFAULT_SETTINGS: TodoPluginSettings = {
onlyRootTasks: false,
};

export interface TodoPluginSettings {
onlyRootTasks: boolean;
}

export class TodoPluginSettingTab extends PluginSettingTab {
plugin: TodoPlugin;
app: App;

constructor(app: App, plugin: TodoPlugin) {
super(app, plugin);
this.plugin = plugin;
this.app = app;
}

display(): void {
let {containerEl} = this;

containerEl.empty();

containerEl.createEl('h2', {text: 'Obsidian TODO | Text-based GTD'});
containerEl.createEl('p', {
text: 'Collects all outstanding TODOs from your vault and presents them in lists Today, Scheduled, Inbox and Someday/Maybe.',
});
containerEl.createEl('h3', {text: 'General Settings'});

new Setting(containerEl)
.setName('Only show root tasks')
.setDesc('If enabled, subtasks (i.e. indented) will not be shown. If disabled, they will.')
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings.onlyRootTasks);
toggle.onChange(async (value) => {
this.plugin.settings.onlyRootTasks = value;
await this.plugin.saveSettings();
});
});
}
}
File renamed without changes.
File renamed without changes.