diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..4eb5808 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +src/events.ts diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..b0cf100 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,17 @@ +module.exports = { + env: { + node: true, + }, + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + }, + plugins: ['@typescript-eslint'], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + }, +} diff --git a/README.md b/README.md index 3cbf8bf..4e463bc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # coc-todolist -Todolist/task manager extension for [coc.nvim](https://github.com/neoclide/coc.nvim) +Todolist manager extension for [coc.nvim](https://github.com/neoclide/coc.nvim) ![](https://user-images.githubusercontent.com/20282795/61593014-d1be3780-ac0c-11e9-96cc-e3b787a27f46.png) @@ -13,72 +13,21 @@ Todolist/task manager extension for [coc.nvim](https://github.com/neoclide/coc.n ## Features - Allow to set a reminder for a todo item -- Auto sync your todolist with gist(require github token: [click here to generate](https://github.com/settings/tokens/new?scopes=gist&description=coc-todolist%20gist)) -- Manage your todolist using CocList +- Auto upload/download todolists with gist(require github token: [click to generate](https://github.com/settings/tokens/new?scopes=gist&description=coc-todolist%20gist)) +- Manage your todolist with CocList ## Configuration ```jsonc -"todolist.enable": { - "type": "boolean", - "default": true, - "description": "whether enable this extension" -}, -"todolist.maxsize": { - "type": "number", - "default": 5000, - "description": "maxsize of todolist" -}, "todolist.dateFormat": { "type": "string", "default": "YYYY-MM-DD HH:mm", "description": "dates format" }, -"todolist.autoUpload": { - "type": "boolean", - "default": false, - "description": "upload your todolist every day" -}, "todolist.monitor": { "type": "boolean", "default": false, "description": "monitor the todolist and remind you at the time" -}, -"todolist.promptForReminder": { - "type": "boolean", - "default": true, - "description": "whether to ask users to set a reminder for every new todo item" -}, -"todolist.easyMode": { - "type": "boolean", - "default": false, - "description": "Open a todo edit window when create/edit a todo item" -}, -"todolist.floatwin.background": { - "type": "string", - "default": "", - "description": "notification floating window background(e.g. #000000)" -}, -"todolist.floatwin.winblend": { - "type": "number", - "default": 0, - "description": "opacity of notification floating window" -}, -"todolist.floatwin.width": { - "type": "number", - "default": 30, - "description": "width of notification floating window" -}, -"todolist.notify": { - "type": "string", - "default": "floating", - "description": "how to notify you", - "enum": [ - "floating", - "virtual", - "echo", - "none" - ] } ``` @@ -88,7 +37,6 @@ Todolist/task manager extension for [coc.nvim](https://github.com/neoclide/coc.n - `:CocCommand todolist.upload`: upload todolist to gist - `:CocCommand todolist.download`: download todolist from gist - `:CocCommand todolist.export`: export todolist as a json/yaml file -- `:CocCommand todolist.closeNotice`: close notifications - `:CocCommand todolist.clear`: clear all todos - `:CocCommand todolist.browserOpenGist`: open todolist gist in [gist.github.com](https://gist.github.com/) @@ -106,13 +54,26 @@ run `:CocList todolist` to open the todolist Q: Where is the todolist data stored? -A: Normally the data is saved in `~/.config/coc/extensions/coc-todolist-data/`, but if you set `g:coc_extension_root` to another location, it will change as well +A: Normally the data is saved in `~/.config/coc/extensions/coc-todolist-data/`, +but if you set `g:coc_extension_root` to another location, it will change as +well -## License +Q: coc-todolist is not loaded after upgrading -MIT +A: Remove `todolist.json`(normally +`~/.config/coc/extensions/coc-todolist-data/todolist.json`). Don't forget to +backup it if necessary. -## More Demos +Q: I want to create a persistent todolist item -![](https://user-images.githubusercontent.com/20282795/84150252-2d4b1b00-aa94-11ea-8701-b0be44f5d507.png) -![](https://user-images.githubusercontent.com/20282795/61623340-08499000-aca9-11e9-9be1-e6d951b075c2.gif) +A: Leave `due` value empty or let `due` be the same as `date` value(default) + +## TODO + +- sync +- Log +- UI + +## License + +MIT diff --git a/autoload/coc_todolist.vim b/autoload/coc_todolist.vim new file mode 100644 index 0000000..80c368e --- /dev/null +++ b/autoload/coc_todolist.vim @@ -0,0 +1,15 @@ +" ============================================================================ +" FileName: coc_todolist.vim +" Author: voldikss +" GitHub: https://github.com/voldikss +" ============================================================================ + +" https://stackoverflow.com/a/26318602/8554147 +function! coc_todolist#get_buf_info() abort + let save_virtualedit = &virtualedit + set virtualedit=all + norm! g$ + let width = virtcol('.') + let &virtualedit = save_virtualedit + return [bufnr('%'), width] +endfunction diff --git a/ftplugin/coc_todolist.vim b/ftplugin/coc_todolist.vim new file mode 100644 index 0000000..a5beafa --- /dev/null +++ b/ftplugin/coc_todolist.vim @@ -0,0 +1,59 @@ +" ============================================================================ +" FileName: coc_todolist.vim +" Author: voldikss +" GitHub: https://github.com/voldikss +" ============================================================================ + +let s:pattern = '─\w\+─' + +function! s:map_down() abort + if search(s:pattern, 'W') != 0 + normal! j + normal! 0 + endif + call s:rematch() +endfunction + +function! s:map_up() abort + call search(s:pattern, 'bW') + call search(s:pattern, 'bW') + normal! j + normal! 0 + call s:rematch() +endfunction + +function! s:map_gg() abort + normal! gg + call search(s:pattern, 'W') + normal! j + normal! 0 + call s:rematch() +endfunction + +function! s:map_G() abort + normal! G + call search(s:pattern, 'bW') + normal! j + normal! 0 + call s:rematch() +endfunction + +function! s:rematch() abort + call clearmatches() + let toplnum = line('.') + let botlnum = search(s:pattern, 'nW') + if botlnum == 0 + let botlnum = line('$') + else + let botlnum -= 1 + endif + let pattern = join(map(range(toplnum, botlnum), { k,v -> '\%' . v . 'l.*' }), '\|') + call matchadd('CocTodolistSelect', pattern) +endfunction + +nmap j :call map_down() +nmap k :call map_up() +nmap :call map_down() +nmap :call map_up() +nmap gg :call map_gg() +nmap G :call map_G() diff --git a/package.json b/package.json index a566a5e..109c049 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,13 @@ "todolist" ], "engines": { - "coc": "^0.0.74" + "coc": "^0.0.80" }, "scripts": { "clean": "rimraf lib", "watch": "webpack --watch", - "build": "webpack" + "build": "webpack", + "prepare": "npx npm-run-all clean build" }, "activationEvents": [ "*" @@ -27,66 +28,15 @@ "configuration": { "type": "object", "properties": { - "todolist.enable": { - "type": "boolean", - "default": true, - "description": "whether enable this extension" - }, - "todolist.maxsize": { - "type": "number", - "default": 5000, - "description": "maxsize of todolist" - }, "todolist.dateFormat": { "type": "string", "default": "YYYY-MM-DD HH:mm", "description": "dates format" }, - "todolist.autoUpload": { - "type": "boolean", - "default": false, - "description": "upload your todolist every day" - }, "todolist.monitor": { "type": "boolean", "default": false, "description": "monitor the todolist and remind you at the time" - }, - "todolist.promptForReminder": { - "type": "boolean", - "default": true, - "description": "whether to ask users to set a reminder for every new todo item" - }, - "todolist.easyMode": { - "type": "boolean", - "default": false, - "description": "Open a todo edit window when create/edit a todo item" - }, - "todolist.floatwin.background": { - "type": "string", - "default": "", - "description": "notification floating window background(e.g. #000000)" - }, - "todolist.floatwin.winblend": { - "type": "number", - "default": 0, - "description": "opacity of notification floating window" - }, - "todolist.floatwin.width": { - "type": "number", - "default": 30, - "description": "width of notification floating window" - }, - "todolist.notify": { - "type": "string", - "default": "floating", - "description": "how to notify you", - "enum": [ - "floating", - "virtual", - "echo", - "none" - ] } } }, @@ -107,16 +57,12 @@ "title": "export todolist as a json or yaml file", "command": "todolist.export" }, - { - "title": "close notification", - "command": "todolist.closeNotice" - }, { "title": "clear all todos", "command": "todolist.clear" }, { - "title": "clear notification", + "title": "clear open todolist gist", "command": "todolist.browserOpenGist" } ] @@ -124,22 +70,25 @@ "author": "dyzplus@gmail.com", "license": "MIT", "devDependencies": { + "@chemzqm/neovim": "^5.2.10", "@types/js-yaml": "^3.12.5", - "@types/node": "^14.14.10", + "@types/node": "^14.14.16", "@types/uuid": "^8.3.0", + "@typescript-eslint/eslint-plugin": "^4.11.1", + "@typescript-eslint/parser": "^4.11.1", "@voldikss/tsconfig": "^1.0.0", - "@voldikss/tslint-config": "^1.0.6", - "coc.nvim": "^0.0.79", - "js-yaml": "^3.14.0", + "coc.nvim": "^0.0.80", + "eslint": "^7.16.0", + "js-yaml": "^3.14.1", "moment": "^2.29.1", + "moment-timezone": "^0.5.32", "path": "^0.12.7", "request-light": "^0.4.0", "rimraf": "^3.0.2", - "ts-loader": "^8.0.11", - "tslint": "^6.1.3", - "typescript": "^4.1.2", - "uuid": "^8.3.1", - "webpack": "^5.8.0", - "webpack-cli": "^4.2.0" + "ts-loader": "^8.0.12", + "typescript": "^4.1.3", + "uuid": "^8.3.2", + "webpack": "^5.11.1", + "webpack-cli": "^4.3.0" } } diff --git a/plugin/coc_todolist.vim b/plugin/coc_todolist.vim index 3647d62..834a4a8 100644 --- a/plugin/coc_todolist.vim +++ b/plugin/coc_todolist.vim @@ -4,14 +4,18 @@ " GitHub: https://github.com/voldikss " ============================================================================ -augroup coc_todolist +function! s:CocTodolistActionAsync(name, ...) + return call( + \ 'CocActionAsync', + \ extend(['runCommand', 'todolist.' . a:name], a:000) + \ ) +endfunction + +augroup CocTodolistInternal autocmd! - autocmd BufWriteCmd __coc_todolist__ call s:Autocmd('BufWriteCmd', +expand('')) + autocmd BufWriteCmd * call s:CocTodolistActionAsync( + \ 'internal.didVimEvent', + \ 'BufWriteCmd', + \ +expand('') + \ ) augroup END - -function! s:Autocmd(...) abort - if !get(g:,'coc_workspace_initialized', 0) - return - endif - call coc#rpc#notify('CocAutocmd', a:000) -endfunction diff --git a/src/commands/guarder.ts b/src/commands/guarder.ts deleted file mode 100644 index 85832d2..0000000 --- a/src/commands/guarder.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { workspace, Neovim, events } from 'coc.nvim' -import { TodoItem, Notification } from '../types' -import DB from '../util/db' -import FloatWindow from '../ui/floatWindow' -import VirtualText from '../ui/virtualText' -import { Dispose } from '../util/dispose' - -export default class Guarder extends Dispose { - private interval: NodeJS.Timeout - private floating: FloatWindow - private virtual: VirtualText - private nvim: Neovim - - constructor(private todoList: DB, private type: string) { - super() - this.nvim = workspace.nvim - this.floating = new FloatWindow(this.nvim) - this.virtual = new VirtualText(this.nvim) - } - - private async notify(todo: TodoItem): Promise { - const notice: Notification = { - title: 'TodoList Guarder', - content: [ - todo.topic, - new Date(todo.date).toLocaleString(), - new Date(todo.due).toLocaleString() - ] - } - const msg = `TODO: ${todo.topic} ${todo.due ? 'at ' + todo.due : ''}` - - switch (this.type) { - case 'floating': - try { - await this.floating.start(notice) - return - } catch (_e) { - workspace.showMessage(_e) - // does not support floating window - } - case 'virtual': - try { - const buffer = await this.nvim.buffer - const bufnr = buffer.id - const lnum = await this.nvim.call('line', ['.']) - await this.virtual.showInfo(bufnr, lnum, msg) - - // TODO: how to delete this event? - events.on('CursorMoved', async (bufnr, cursor) => { - await this.virtual.showInfo(bufnr, cursor[0], msg) - }) - return - } catch (_e) { - // does not support virtual text - } - case 'echo': - workspace.showMessage(msg) - return - default: - return - } - } - - public async deactive(uid: string, todo: TodoItem): Promise { - todo.active = false - await this.todoList.update(uid, todo) - } - - public async monitor(): Promise { - this.interval = setInterval(async () => { - const todolist = await this.todoList.load() - if (!(todolist?.length > 0)) return - const now = new Date().getTime() - for (const a of todolist) { - const { todo, uid } = a - const { due, active } = todo - if (due && Date.parse(due) <= now && active) { - await this.notify(todo) - await this.deactive(uid, todo) - } - } - }, 1000) // TODO - } - - public async closeNotice(): Promise { - await this.floating.destroy() - await this.virtual.destroy() - } - - public stopGuard(): void { - if (this.interval) clearInterval(this.interval) - } - - public dispose(): void { - // tslint:disable-next-line: no-floating-promises - this.closeNotice() - this.stopGuard() - } -} diff --git a/src/commands/guardian.ts b/src/commands/guardian.ts new file mode 100644 index 0000000..2b08766 --- /dev/null +++ b/src/commands/guardian.ts @@ -0,0 +1,62 @@ +import { window, workspace } from 'coc.nvim' +import { TodoItem } from '../types' +import DB from '../util/db' +import { Dispose } from '../util/dispose' + +export default class Guardian extends Dispose { + private interval: NodeJS.Timeout + + constructor(private todoList: DB) { + super() + const { nvim } = workspace + nvim.command('hi def link CocTodolistNotification Statement', true) + nvim.command('hi def link CocTodolistNotificationBorder Special', true) + } + + private async notify(todo: TodoItem): Promise { + const message = [] + message.push(`Topic: ${todo.topic}`) + if (todo.due != todo.date) { + message.push(`Due: ${new Date(todo.due).toLocaleString()}`) + } + if (todo.detail.length > 0) { + message.push(`'Detail: ${todo.detail}'`) + } + await window.showNotification({ + title: 'coc-todolist notification', + content: message.join('\n'), + close: true, + highlight: 'CocTodolistNotification', + borderhighlight: 'CocTodolistNotificationBorder' + }) + } + + public async deactive(uid: string, todo: TodoItem): Promise { + todo.active = false + await this.todoList.update(uid, todo) + } + + public async monitor(): Promise { + this.interval = setInterval(async () => { + const todolist = await this.todoList.load() + if (!(todolist?.length > 0)) return + const now = new Date().getTime() + for (const a of todolist) { + const { todo, uid } = a + const { due, active } = todo + if (new Date(due).getTime() <= now && due != todo.date && active) { + await this.notify(todo) + await this.deactive(uid, todo) + } + } + }, 1000) // TODO + } + + public stopGuard(): void { + if (this.interval) clearInterval(this.interval) + } + + public dispose(): void { + this.stopGuard() + } +} diff --git a/src/commands/todoer.ts b/src/commands/todoist.ts similarity index 56% rename from src/commands/todoer.ts rename to src/commands/todoist.ts index 0fa0276..6eac195 100644 --- a/src/commands/todoer.ts +++ b/src/commands/todoist.ts @@ -1,65 +1,45 @@ -import { workspace } from 'coc.nvim' +import { window, workspace } from 'coc.nvim' import { TodoItem, GistObject } from '../types' import { fsStat } from '../util/fs' import path from 'path' import DB from '../util/db' import { Gist } from '../service/gists' -import TodolistInfo from '../util/info' -import moment from 'moment' -import { newTodo, createTodoEditBuffer } from '../util/helper' +import Profile from '../profile' +import { createTodo, createTodoEditBuffer } from '../util/helper' -export default class Todoer { +export default class Todoist { private gist: Gist - constructor(private db: DB, private info: TodolistInfo) { + constructor(private db: DB, private profile: Profile) { this.gist = new Gist() } private async getGitHubToken(): Promise { - let token: string = await this.info.fetch('userToken') + let token: string = await this.profile.fetch('userToken') if (!token) { - token = await workspace.requestInput('Input github token') + token = await window.requestInput('Input github token') if (!(token?.trim().length > 0)) return token = token.trim() - await this.info.push('userToken', token) + await this.profile.push('userToken', token) } return token } public async create(): Promise { - const todo = newTodo() - const config = workspace.getConfiguration('todolist') - if (!config.get('easyMode')) { - await createTodoEditBuffer(todo, this.db, 'create') - return - } - const topic = await workspace.requestInput('Input the topic') - if (!(topic?.trim().length > 0)) return - todo.topic = topic.trim() - if (config.get('promptForReminder')) { - const remind = await workspace.requestInput('Set a due date?(y/N)') - if (remind?.trim().toLowerCase() === 'y') { - const dateFormat = config.get('dateFormat') - let dueDate = moment().format(dateFormat) - dueDate = await workspace.requestInput('When to remind you', dueDate) - if (!(dueDate?.trim().length > 0)) return - todo.due = moment(dueDate.trim(), dateFormat).toDate().toString() - } - } - await this.db.add(todo) - workspace.showMessage('New todo added') + const todo = createTodo() + await createTodoEditBuffer(todo, this.db, 'create') } public async download(directory: string): Promise { - let statusItem = workspace.createStatusBarItem(0, { progress: true }) + const statusItem = window.createStatusBarItem(0, { progress: true }) statusItem.text = 'downloading todolist' // if gist id exists, use that to download gist - let gistid: string = await this.info.fetch('gistId') + let gistid: string = await this.profile.fetch('gistId') if (!(gistid?.trim().length > 0)) { - gistid = await workspace.requestInput('Input gist id') + gistid = await window.requestInput('Input gist id') if (!(gistid?.trim().length > 0)) return gistid = gistid.trim() - await this.info.push('gistId', gistid) + await this.profile.push('gistId', gistid) } statusItem.show() @@ -80,23 +60,23 @@ export default class Todoer { if (content) { const todos: TodoItem[] = JSON.parse(content) await this.db.updateAll(todos) - workspace.showMessage('Downloaded todolist from gist') + window.showMessage('Downloaded todolist from gist') } // update github username - await this.info.push('userName', gist.owner.login) + await this.profile.push('userName', gist.owner.login) } else if (res.status === 404) { - workspace.showMessage('Remote gist was not found', 'error') - await this.info.delete('gistId') + window.showMessage('Remote gist was not found', 'error') + await this.profile.delete('gistId') return } else { - workspace.showMessage('Downloading error', 'error') + window.showMessage('Downloading error', 'error') return } } public async upload(): Promise { - let statusItem = workspace.createStatusBarItem(0, { progress: true }) + const statusItem = window.createStatusBarItem(0, { progress: true }) statusItem.text = 'uploading todolist' this.gist.token = await this.getGitHubToken() // TODO @@ -115,47 +95,39 @@ export default class Todoer { const data = Buffer.from(JSON.stringify(gistObj)) // If gistId exists, upload - let gistId: string = await this.info.fetch('gistId') + const gistId: string = await this.profile.fetch('gistId') if (gistId?.trim().length > 0) { statusItem.show() const res = await this.gist.patch(`/gists/${gistId}`, data) statusItem.hide() if (res.status === 200) { - workspace.showMessage('Updated gist todolist') - await this.updateLog() + window.showMessage('Updated gist todolist') // update github username const gist: GistObject = JSON.parse(res.responseText) - await this.info.push('userName', gist.owner.login) + await this.profile.push('userName', gist.owner.login) return } else if (res.status !== 404) { - workspace.showMessage('Failed to uploaded todo gist', 'error') + window.showMessage('Failed to uploaded todo gist', 'error') return } else { // 404: delete gistId and create a new gist - workspace.showMessage('Remote gist was not found', 'error') - await this.info.delete('gistId') + window.showMessage('Remote gist was not found', 'error') + await this.profile.delete('gistId') } } // gistId doesn't exists, fallback to creating const res = await this.gist.post('/gists', data) if (res.status == 201 && res.responseText) { const gist: GistObject = JSON.parse(res.responseText) - await this.info.push('gistId', gist.id) - workspace.showMessage('Uploaded a new todolist to gist') - await this.updateLog() + await this.profile.push('gistId', gist.id) + window.showMessage('Uploaded a new todolist to gist') // update github username - await this.info.push('userName', gist.owner.login) + await this.profile.push('userName', gist.owner.login) } else { - workspace.showMessage('Failed to create todolist from gist', 'error') + window.showMessage('Failed to create todolist from gist', 'error') return } } - private async updateLog(): Promise { - const now = new Date() - const day = new Date(now.getFullYear(), now.getMonth(), now.getDate()) - await this.info.push('lastUpload', day.getTime()) - } - public async export(): Promise { const { nvim } = workspace const arr = await this.db.load() @@ -182,6 +154,6 @@ export default class Todoer { public async clear(): Promise { await this.db.clear() - workspace.showMessage('All todos were cleared') + window.showMessage('All todos were cleared') } } diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 0000000..f172623 --- /dev/null +++ b/src/events.ts @@ -0,0 +1,56 @@ +import { commands, Disposable, ExtensionContext } from 'coc.nvim' + +type Arguments = F extends (...args: infer Args) => any + ? Args + : never + +class EventListener< + F extends (...args: A) => void | Promise, + A extends any[] = Arguments + > { + listeners: F[] = [] + + on(func: F, disposables?: Disposable[]) { + this.listeners.push(func) + const disposable = Disposable.create(() => { + const index = this.listeners.indexOf(func) + if (index !== -1) { + this.listeners.splice(index, 1) + } + }) + if (disposables) { + disposables.push(disposable) + } + return disposable + } + + fire(...args: A) { + this.listeners.forEach(async (listener) => { + try { + await listener(...args) + } catch (e) { + // nop + } + }) + } +} + +export const bufWriteCmdListener = new EventListener<(bufnr: number) => void>() + +const internalEventHanders: Record<'BufWriteCmd', (...args: any[]) => void> = { + BufWriteCmd(args: [number]) { + bufWriteCmdListener.fire(...args) + }, +} + +export function registerVimInternalEvents(context: ExtensionContext): void { + context.subscriptions.push( + commands.registerCommand( + 'todolist.internal.didVimEvent', + (event: keyof typeof internalEventHanders, ...args: any[]) => + internalEventHanders[event](args), + undefined, + true + ) + ) +} diff --git a/src/index.ts b/src/index.ts index bcdc5c0..ff0f27e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,104 +1,75 @@ -import { - commands, - ExtensionContext, - listManager, - workspace -} from 'coc.nvim' +import { commands, ExtensionContext, listManager, window, workspace } from 'coc.nvim' import { fsMkdir, fsStat } from './util/fs' import TodoList from './lists/todolist' -import Todoer from './commands/todoer' +import Todoist from './commands/todoist' +import Guardian from './commands/guardian' import DB from './util/db' -import Guarder from './commands/guarder' -import TodolistInfo from './util/info' +import Profile from './profile' +import { registerVimInternalEvents } from './events' export async function activate(context: ExtensionContext): Promise { - const config = workspace.getConfiguration('todolist') - const enable = config.get('enable', true) - if (!enable) return - - const maxsize = config.get('maxsize', 5000) - const monitor = config.get('monitor', false) - const autoUpload = config.get('autoUpload', false) - const type = config.get('notify', 'floating') - const { subscriptions, storagePath } = context const { nvim } = workspace - const stat = await fsStat(storagePath) if (!(stat?.isDirectory())) { await fsMkdir(storagePath) } - const db = new DB(storagePath, 'todolist', maxsize) - const info = new TodolistInfo(storagePath) - const todoer = new Todoer(db, info) - const guarder = new Guarder(db, type) - subscriptions.push(guarder) - - if (monitor) await guarder.monitor() - if (autoUpload) { - const now = new Date() - const day = new Date(now.getFullYear(), now.getMonth(), now.getDate()) - const last = await info.fetch('lastUpload') - if (last && Number(last) < day.getTime()) { - workspace.showMessage('uploading') - await todoer.upload() - } + const rtp = (await nvim.getOption('runtimepath')) as string + const paths = rtp.split(',') + if (!paths.includes(context.extensionPath)) { + await nvim.command( + `execute 'noa set rtp^='.fnameescape('${context.extensionPath.replace( + /'/g, + "''", + )}')`, + ) } - // thank to weirongxu/coc-explorer - (async () => { - const rtp = (await nvim.getOption('runtimepath')) as string - const paths = rtp.split(',') - if (!paths.includes(context.extensionPath)) { - await nvim.command( - `execute 'noa set rtp^='.fnameescape('${context.extensionPath.replace( - /'/g, - "''", - )}')`, - ) - } - })().catch(_e => { }) + registerVimInternalEvents(context) + + const db = new DB(storagePath, 'todolist') + const profile = new Profile(storagePath) + const todo = new Todoist(db, profile) + + const guard = new Guardian(db) + subscriptions.push(guard) + if (workspace.getConfiguration('todolist.monitor')) { + await guard.monitor() + } subscriptions.push( commands.registerCommand( 'todolist.create', - async () => await todoer.create() + async () => await todo.create() ) ) subscriptions.push( commands.registerCommand( 'todolist.upload', - async () => await todoer.upload() + async () => await todo.upload() ) ) subscriptions.push( commands.registerCommand( 'todolist.download', - async () => await todoer.download(storagePath) + async () => await todo.download(storagePath) ) ) subscriptions.push( commands.registerCommand( 'todolist.export', - async () => await todoer.export() + async () => await todo.export() ) ) subscriptions.push( commands.registerCommand( 'todolist.clear', - async () => await todoer.clear() - ) - ) - - subscriptions.push( - commands.registerCommand( - 'todolist.closeNotice', - async () => await guarder.closeNotice() + async () => await todo.clear() ) ) @@ -106,13 +77,13 @@ export async function activate(context: ExtensionContext): Promise { commands.registerCommand( 'todolist.browserOpenGist', async () => { - const userName = await info.fetch('userName') - const gistId = await info.fetch('gistId') + const userName = await profile.fetch('userName') + const gistId = await profile.fetch('gistId') if (userName && gistId) { const url = `https://gist.github.com/${userName}/${gistId}` nvim.call('coc#util#open_url', url, true) } else { - workspace.showMessage('userName or gistId not found', 'error') + window.showMessage('userName or gistId not found', 'error') } } ) diff --git a/src/lists/todolist.ts b/src/lists/todolist.ts index d90a593..ce5de9b 100644 --- a/src/lists/todolist.ts +++ b/src/lists/todolist.ts @@ -1,11 +1,4 @@ -import { - ListAction, - ListContext, - ListItem, - BasicList, - Neovim, - workspace, -} from 'coc.nvim' +import { ListAction, ListItem, BasicList, Neovim, workspace, window } from 'coc.nvim' import { TodoItem, TodoData } from '../types' import DB from '../util/db' import moment from 'moment' @@ -38,8 +31,8 @@ export default class TodoList extends BasicList { lines.push(`Date: ${todo.date}`) lines.push(`Active: ${todo.active}`) lines.push(`Due: ${todo.due}`) - lines.push(`Description:`) - lines.push(...todo.description.split('\n')) + lines.push(`Detail:`) + lines.push(...todo.detail.split('\n')) await this.preview({ bufname: 'todolist', sketch: true, @@ -51,26 +44,8 @@ export default class TodoList extends BasicList { this.addAction('edit', async (item: ListItem) => { const { uid } = item.data as TodoData const todo = item.data.todo - const config = workspace.getConfiguration('todolist') - if (!config.get('easyMode')) { - await createTodoEditBuffer(todo, this.db, 'update', uid) - return - } - const topic = await workspace.requestInput('Input new topic', todo.topic) - if (!(topic?.trim().length > 0)) return - todo.topic = topic.trim() - if (config.get('promptForReminder')) { - const remind = await workspace.requestInput('Set a due date?(y/N)') - if (remind?.trim().toLowerCase() === 'y') { - const dateFormat = config.get('dateFormat') - let dueDate = moment().format(dateFormat) - dueDate = await workspace.requestInput('When to remind you', dueDate) - if (!(dueDate?.trim().length > 0)) return - todo.due = moment(dueDate.trim(), dateFormat).toDate().toString() - } - } - await this.db.update(uid, todo) - workspace.showMessage('Todo item updated') + await createTodoEditBuffer(todo, this.db, 'update', uid) + return }) this.addAction('delete', async (item: ListItem) => { @@ -86,13 +61,13 @@ export default class TodoList extends BasicList { return priority.get(a.active) - priority.get(b.active) } - public async loadItems(_context: ListContext): Promise { + public async loadItems(): Promise { const arr = await this.db.load() let res: ListItem[] = [] for (const item of arr) { - const { topic, active, description } = item.todo + const { topic, active, detail } = item.todo const shortcut = active ? '[*]' : '[√]' - const label = `${shortcut} ${topic} \t ${description ? description : ''}` + const label = `${shortcut} ${topic} \t ${detail ? detail : ''}` res.push({ label, filterText: topic, @@ -112,7 +87,7 @@ export default class TodoList extends BasicList { nvim.command('hi def link CocTodolistStatus Constant', true) nvim.command('hi def link CocTodolistTopic String', true) nvim.command('hi def link CocTodolistDescription Comment', true) - nvim.resumeNotification().catch(_e => { + nvim.resumeNotification().catch(() => { // nop }) } diff --git a/src/util/info.ts b/src/profile.ts similarity index 71% rename from src/util/info.ts rename to src/profile.ts index 5b58e7e..dc40b86 100644 --- a/src/util/info.ts +++ b/src/profile.ts @@ -1,22 +1,22 @@ -import { fsReadFile, fsStat, fsWriteFile } from './fs' +import { fsReadFile, fsStat, fsWriteFile } from './util/fs' import fs from 'fs' import path from 'path' -export default class TodolistInfo { +export default class Profile { private file: string constructor(directory: string) { this.file = path.join(directory, 'config.json') } - public async load(): Promise { - let stat = await fsStat(this.file) - if (!(stat.isFile())) { + public async load(): Promise> { + const stat = await fsStat(this.file) + if (!(stat?.isFile())) { await fsWriteFile(this.file, '{}') return {} } try { - let content = await fsReadFile(this.file) + const content = await fsReadFile(this.file) return JSON.parse(content) } catch (e) { await fsWriteFile(this.file, '{}') @@ -25,7 +25,7 @@ export default class TodolistInfo { } public async fetch(key: string): Promise { - let obj = await this.load() + const obj = await this.load() if (typeof obj[key] == 'undefined') { return undefined } @@ -33,7 +33,7 @@ export default class TodolistInfo { } public async exists(key: string): Promise { - let obj = await this.load() + const obj = await this.load() if (typeof obj[key] == 'undefined') { return false } @@ -41,7 +41,7 @@ export default class TodolistInfo { } public async delete(key: string): Promise { - let obj = await this.load() + const obj = await this.load() if (typeof obj[key] == 'undefined') { return } @@ -50,14 +50,14 @@ export default class TodolistInfo { } public async push(key: string, data: number | null | boolean | string): Promise { - let obj = await this.load() + const obj = await this.load() obj[key] = data await fsWriteFile(this.file, JSON.stringify(obj, null, 2)) } public async clear(): Promise { - let stat = await fsStat(this.file) - if (!(stat.isFile())) return + const stat = await fsStat(this.file) + if (!(stat?.isFile())) return await fsWriteFile(this.file, '{}') } diff --git a/src/service/gists.ts b/src/service/gists.ts index 94b03b6..3acbc91 100644 --- a/src/service/gists.ts +++ b/src/service/gists.ts @@ -20,7 +20,7 @@ export class Gist { return await this.request('PATCH', path, data) } - public async request(method: string, path, data?: Buffer): Promise { + public async request(method: string, path: string, data?: Buffer): Promise { const headers = { 'Accept-Encoding': 'gzip, deflate', 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36' diff --git a/src/types.ts b/src/types.ts index c27d191..e063993 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,7 @@ export interface TodoItem { date: string active: boolean due: string - description: string + detail: string } export interface TodoData { diff --git a/src/ui/floatWindow.ts b/src/ui/floatWindow.ts deleted file mode 100644 index 127a80f..0000000 --- a/src/ui/floatWindow.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { FloatOptions } from '@chemzqm/neovim/lib/api/types' -import { Buffer } from '@chemzqm/neovim/lib/api/Buffer' -import { Window, Neovim, workspace, WorkspaceConfiguration } from 'coc.nvim' -import { Notification } from '../types' -import Highlighter from 'coc.nvim/lib/model/highligher' - -export default class FloatWindow { - private window: Window = null - private windows: Window[] = [] - private config: WorkspaceConfiguration - private tempWins: Window[] = [] - private width = 30 - private height = 4 - - constructor(private nvim: Neovim) { - this.config = workspace.getConfiguration('todolist.floatwin') - workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('todolist.floatwin')) { - this.config = workspace.getConfiguration('todolist.floatwin') - } - }) - this.width = this.config.get('width', 30) - } - - private async getWinConfig(): Promise { - const height = await this.nvim.getOption('lines') - const width = await this.nvim.getOption('columns') - - const winConfig: FloatOptions = { - focusable: false, - relative: 'editor', - anchor: 'NW', - height: this.height, - width: this.width, - row: Number(height) + 1, - col: Number(width) - this.width, - } - - return winConfig - } - - private async render(notice: Notification): Promise { - const hl = new Highlighter() - const margin = ' '.repeat(Math.floor((this.width - notice.title.length) / 2)) - hl.addLine(`${margin}${notice.title}`, 'Title') - for (const detail of notice.content) { - hl.addLine('* ') - hl.addText(detail, 'String') - } - return hl - } - - private async moveUp(windows: Window[]): Promise { - await Promise.all(windows.map(async w => { - const isValid = await w.valid - if (isValid) { - const winConfig = await w.getConfig() - for (let i = 0; i <= this.height; i++) { - winConfig.row -= 1 - await w.setConfig(winConfig) - await new Promise(resolve => setTimeout(resolve, 30)) - } - } - })) - } - - /** - * keep windows count when there are over one notifications - */ - private async noMoreWins(): Promise { - const winblend = this.config.get('winblend', 0) - - // only one window preserved - if (this.windows.length > 1) { - const discard = this.windows.shift() - this.tempWins.push(discard) - setImmediate(async () => { - for (let i = winblend; i <= 100; i++) { - await discard.setOption('winblend', i) - await new Promise( - resolve => setTimeout(resolve, 500 / (100 - winblend)) - ) - } - await discard.close(true) - this.tempWins.shift() - }) - } - } - - private async create(buf: Buffer): Promise { - const winConfig = await this.getWinConfig() - this.window = await this.nvim.openFloatWindow(buf, false, winConfig) - this.windows.push(this.window) - - const { window, nvim } = this - - nvim.pauseNotification() - const winblend = this.config.get('winblend', 0) - const floatwinBg = this.config.get('background') - if (floatwinBg) { - nvim.command(`hi TodoGuarder guibg=${floatwinBg}`, true) - } else { - nvim.command(`hi def link TodoGuarder NormalFloat`, true) - } - window.setOption('winhighlight', 'NormalFloat:TodoGuarder,Normal:TodoGuarder,FoldColumn:TodoGuarder', true) - window.setOption('number', false, true) - window.setOption('relativenumber', false, true) - window.setOption('cursorline', false, true) - window.setOption('signcolumn', 'no', true) - window.setOption('foldcolumn', 1, true) - window.setOption('winblend', winblend, true) - await nvim.resumeNotification() - - await this.moveUp(this.windows.concat(this.tempWins)) - - await this.noMoreWins() - } - - public async start(notice: Notification): Promise { - const buf = await this.nvim.createNewBuffer() - buf.setOption('bufhidden', 'wipe', true) - buf.setOption('buftype', 'nowrite', true) - - const renderer = await this.render(notice) - // buffer was changed here - renderer.render(buf) - await this.create(buf) - } - - public async destroy(): Promise { - for (const w of this.windows.concat(this.tempWins)) { - const isValid = await w.valid - if (isValid) await w.close(true) - } - } -} diff --git a/src/ui/virtualText.ts b/src/ui/virtualText.ts deleted file mode 100644 index 763c385..0000000 --- a/src/ui/virtualText.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Neovim, workspace } from 'coc.nvim' - -export default class VirtualText { - private show = true - private virtualTextSrcId: number - - constructor(private nvim: Neovim) { } - - public async showInfo(bufnr: number, lnum: number, text: string): Promise { - if (!this.show) return - - const doc = workspace.getDocument(bufnr) - if (doc && this.virtualTextSrcId) { - doc.buffer.clearNamespace(this.virtualTextSrcId, 0, -1) - } - - this.virtualTextSrcId = await this.nvim.createNamespace('coc-todolist') - const buffer = await this.nvim.buffer - await buffer.setVirtualText(this.virtualTextSrcId, lnum - 1, [[text, 'WarningMsg']]) - } - - public async destroy(): Promise { - const buffer = await this.nvim.buffer - const doc = workspace.getDocument(buffer.id) - if (doc && this.virtualTextSrcId) { - doc.buffer.clearNamespace(this.virtualTextSrcId, 0, -1) - this.show = false - } - } -} diff --git a/src/util/db.ts b/src/util/db.ts index a0f57c0..8fd91f3 100644 --- a/src/util/db.ts +++ b/src/util/db.ts @@ -6,34 +6,27 @@ import uuid = require('uuid') export default class DB { private file: string - constructor( - directory: string, - name: string, - private maxsize: number - ) { + constructor(directory: string, name: string) { this.file = path.join(directory, `${name}.json`) } public async load(): Promise { - let stat = await fsStat(this.file) + const stat = await fsStat(this.file) if (!(stat?.isFile())) return [] - let content = await fsReadFile(this.file) + const content = await fsReadFile(this.file) return JSON.parse(content) as TodoData[] } public async add(data: TodoItem): Promise { - let items = await this.load() - if (items.length == this.maxsize) { - items.pop() - } + const items = await this.load() items.unshift({ uid: uuid.v4(), todo: data }) await fsWriteFile(this.file, JSON.stringify(items, null, 2)) } public async delete(uid: string): Promise { - let items = await this.load() - let idx = items.findIndex(o => o.uid == uid) + const items = await this.load() + const idx = items.findIndex(o => o.uid == uid) if (idx !== -1) { items.splice(idx, 1) await fsWriteFile(this.file, JSON.stringify(items, null, 2)) @@ -41,8 +34,8 @@ export default class DB { } public async update(uid: string, data: TodoItem): Promise { - let items = await this.load() - let idx = items.findIndex(o => o.uid == uid) + const items = await this.load() + const idx = items.findIndex(o => o.uid == uid) if (idx !== -1) { items[idx].todo = data await fsWriteFile(this.file, JSON.stringify(items, null, 2)) diff --git a/src/util/helper.ts b/src/util/helper.ts index 5e68f0b..939f941 100644 --- a/src/util/helper.ts +++ b/src/util/helper.ts @@ -1,72 +1,88 @@ import { TodoItem } from "../types" -import { workspace, events } from "coc.nvim" +import { workspace, window } from "coc.nvim" import DB from "./db" +import { bufWriteCmdListener } from "../events" +import moment from "moment-timezone" -let alreadyAdded = false +const dateFormat = workspace.getConfiguration('todolist').get('dateFormat') + +let todolistUpdated = false let bufChanged = false -export function newTodo(): TodoItem { +export function createTodo(): TodoItem { + const date = new Date().toString() return { topic: '', - date: new Date().toString(), + date: date, active: true, - due: '', - description: '' + due: date, + detail: '' } as TodoItem } export function parseTodo(text: string): TodoItem { - const lines = text.split('\n') - const todo = newTodo() - for (const line of lines) { - if (/__TOPIC__/.test(line)) { - todo.topic = line.substr(13).trim() - } else if (/__DATE__/.test(line)) { - todo.date = line.substr(13).trim() - } else if (/__ACTIVE__/.test(line)) { - todo.active = line.substr(13).trim() == 'true' - } else if (/__DUE__/.test(line)) { - todo.due = line.substr(13).trim() - } else if (/__DESCRIPTION__/.test(line)) { - todo.description = '' - } else if (!(/───────/.test(line))) { - todo.description += line + '\n' + const lines = text.trim().split('\n') + const todo = createTodo() + let flag + for (const line of lines.slice(3)) { + if (/^\s*$/.test(line)) { + continue + } else if (/─TOPIC─/.test(line)) { + flag = 'topic' + } else if (/─DETAIL─/.test(line)) { + flag = 'detail' + } else if (/─DATE─/.test(line)) { + flag = 'date' + } else if (/─DUE─/.test(line)) { + flag = 'due' + } else if (/─ACTIVE─/.test(line)) { + flag = 'active' + } else { + if (flag == 'topic' || flag == 'detail') { + todo[flag] += line.trim() + } else if (flag == 'date' || flag == 'due') { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone + todo[flag] = moment(line.trim(), dateFormat).tz(timezone).toDate().toString() + } else { + todo[flag] = line.trim() == 'true' + } } } return todo } export function drawTodo(todo: TodoItem, width: number): string[] { + const drawBar = (title: string, width: number) => { + return ('─'.repeat((width - title.length) / 2) + title).padEnd(width, '─') + } const res = [] - const com = `─────────────${'─'.repeat(width - 19)}` - const top = `────────────┬${'─'.repeat(width - 19)}` - const cen = `────────────┼${'─'.repeat(width - 19)}` - const bot = `────────────┴${'─'.repeat(width - 19)}` res.push(`Save current buffer to create/update todolist`) - res.push(``) - res.push(top) - res.push(` __TOPIC__ │ ${todo.topic}`) - res.push(cen) - res.push(` __DATE__ │ ${todo.date}`) - res.push(cen) - res.push(` __ACTIVE__ │ ${todo.active}`) - res.push(cen) - res.push(` __DUE__ │ ${todo.due}`) - res.push(bot) - res.push(` __DESCRIPTION__`) - res.push(com) - res.push(...todo.description.trim().split('\n')) - res.push(com) + res.push(`Notice that this buffer will be closed after saving`) + res.push('') + res.push(drawBar('ACTIVE', width)) + res.push(todo.active) + res.push(drawBar('DATE', width)) + res.push(moment(todo.date).format(dateFormat)) + res.push(drawBar('DUE', width)) + res.push(moment(todo.due).format(dateFormat)) + res.push(drawBar('TOPIC', width)) + res.push(todo.topic) + res.push(drawBar('DETAIL', width)) + res.push(...todo.detail.trim().split('\n')) return res } -export function isValid(todo: TodoItem): boolean { +export function checkTodo(todo: TodoItem): boolean { if (todo.topic.trim() == '') { - workspace.showMessage('topic can not be empty', 'error') + window.showMessage('TOPIC can not be empty', 'error') return false } - if (Date.parse(todo.date).toString() == 'NaN') { - workspace.showMessage('error date format', 'error') + if (todo.date.toString() == 'NaN') { + window.showMessage('error on DATE format', 'error') + return false + } + if (todo.due.toString() == 'NaN') { + window.showMessage('error on DUE format', 'error') return false } return true @@ -80,56 +96,50 @@ export async function createTodoEditBuffer( ): Promise { const { nvim } = workspace await nvim.command('runtime plugin/coc_todolist.vim') - const openCommand = workspace.getConfiguration('refactor').get('openCommand') as string nvim.pauseNotification() - nvim.command(`${openCommand} __coc_todolist__`, true) + nvim.command(`vs __coc_todolist__`, true) nvim.command(`setl filetype=coc_todolist buftype=acwrite nobuflisted bufhidden=wipe nofen wrap`, true) nvim.command(`setl undolevels=1000 nolist nospell noswapfile nomod conceallevel=2 concealcursor=n`, true) - nvim.command('setl nomod', true) - const [, err] = await nvim.resumeNotification() - if (err) { - logger.error(err) - workspace.showMessage(`Error on open todoedit window: ${err}`, 'error') - return - } - const [bufnr, width] = await nvim.eval('[bufnr("%"), winwidth(0)]') as [number, number] + nvim.command('setl nomod nonumber norelativenumber signcolumn=yes:1', true) + await nvim.resumeNotification() + + const [bufnr, width] = await nvim.call('coc_todolist#get_buf_info') as [number, number] const lines = drawTodo(todo, width) await nvim.call('append', [0, lines]) await nvim.command('normal! Gdd') - await nvim.call('cursor', [4, 0]) - await nvim.command('normal! A') - alreadyAdded = false + await nvim.call('search', ['TOPIC', 'b']) + await nvim.command('normal! j') + await nvim.command('normal! I') + todolistUpdated = false - // @todo - // use coc's runCommand - // https://github.com/neoclide/coc.nvim/issues/2054 - events.on('BufWriteCmd', async () => { - if (alreadyAdded) { - workspace.showMessage('Todo item already added', 'warning') + bufWriteCmdListener.on(async () => { + if (todolistUpdated) { + window.showMessage('Todo item already added', 'warning') return } if (!bufChanged) { - workspace.showMessage('No changes', 'warning') + window.showMessage('No changes', 'warning') return } const document = workspace.getDocument(bufnr) if (document) { const lines = document.content const todo = parseTodo(lines) - if (this.isValid(todo)) { + if (checkTodo(todo)) { if (action == 'create') { await db.add(todo) - workspace.showMessage('New todo added') + window.showMessage('New todo added') } else { await db.update(uid, todo) - workspace.showMessage('Todo item updated') + window.showMessage('Todo item updated') } - alreadyAdded = true + todolistUpdated = true + nvim.command('close!') } } }) workspace.onDidChangeTextDocument(() => { bufChanged = true - alreadyAdded = false + todolistUpdated = false }) } diff --git a/syntax/coc_todolist.vim b/syntax/coc_todolist.vim index 3e327a6..a70e97c 100644 --- a/syntax/coc_todolist.vim +++ b/syntax/coc_todolist.vim @@ -7,22 +7,13 @@ if exists('b:current_syntax') finish endif +let b:current_syntax = 'coc_todolist' +syntax match CocTodolistKeyword '\(TOPIC\|DATE\|ACTIVE\|DUE\|DETAIL\)' +syntax match CocTodolistComment '\%<3l.*' +syntax match CocTodolistDelimiter '─' -syntax match CocTodolistKeyword #^\C\s\zs__\(TOPIC\|DATE\|ACTIVE\|DUE\|DESCRIPTION\)__# -syntax match CocTodolistComment #\%1l.*# -syntax match CocTodolistDelimiter #\(─\|│\|┴\|┼\|┬\)# -syntax match CocTodolistTime #[0-9:]\+# - -syntax keyword CocTodolistTrue true -syntax keyword CocTodolistFalse false - -hi def link CocTodolistKeyword Keyword -hi def link CocTodolistComment Comment -hi def link CocTodolistDelimiter Special -hi def link CocTodolistTime Number -hi def link CocTodolistTrue Constant -hi def link CocTodolistFalse Constant - - -let b:current_syntax = '__coc_todolist__' +hi def link CocTodolistKeyword Keyword +hi def link CocTodolistComment Comment +hi def link CocTodolistDelimiter Special +hi def link CocTodolistSelect Search diff --git a/tslint.json b/tslint.json deleted file mode 100644 index 8d3e06a..0000000 --- a/tslint.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "./node_modules/@voldikss/tslint-config/tslint.json", - "rules": { - }, - "linterOptions": { - "exclude": [ - ] - } -}