import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; import isBetween from 'dayjs/plugin/isBetween'; import isoWeek from 'dayjs/plugin/isoWeek'; import relativeTime from 'dayjs/plugin/relativeTime'; import throttle from 'lodash/throttle'; import { ConfigurationChangeEvent, Disposable, ExtensionContext, Range, TextDocument, TextEditorDecorationType, window, workspace } from 'vscode'; import { registerAllCommands } from './commands'; import { updateEditorDecorationStyle } from './decorations'; import { resetAllRecurringTasks } from './documentActions'; import { checkIfNeedResetRecurringTasks, onChangeActiveTextEditor } from './events'; import { updateLanguageFeatures } from './languageFeatures/languageFeatures'; import { parseDocument } from './parse'; import { CounterStatusBar, MainStatusBar } from './statusBar'; import { TheTask } from './TheTask'; import { createAllTreeViews, groupAndSortTreeItems, updateAllTreeViews, updateArchivedTasks } from './treeViewProviders/treeViews'; import { ExtensionConfig, ItemForProvider, VscodeContext } from './types'; import { updateUserSuggestItems } from './userSuggestItems'; import { getActiveDocument, getDocumentForDefaultFile } from './utils/extensionUtils'; import { getEditorLineHeight, setContext } from './utils/vscodeUtils'; import { TasksWebviewViewProvider } from './webview/webviewView'; dayjs.extend(isBetween); dayjs.extend(relativeTime); dayjs.extend(isoWeek); dayjs.extend(duration); dayjs.Ls.en.weekStart = 1; /** * Things extension keeps a global reference to and uses extensively */ export abstract class $state { /** All tasks (not as tree) */ static tasks: TheTask[] = []; /** Tasks in a tree format (`task.subtasks` contains nested items) */ static tasksAsTree: TheTask[] = []; /** All archived tasks */ static archivedTasks: TheTask[] = []; /** All tags */ static tags: string[] = []; /** All projects */ static projects: string[] = []; /** All contexts */ static contexts: string[] = []; static suggestTags: Record<string, string> = {}; static suggestProjects: Record<string, string> = {}; static suggestContexts: Record<string, string> = {}; /** Tags sorted and grouped for tags Tree View */ static tagsForTreeView: ItemForProvider[] = []; /** Projects sorted and grouped for projects Tree View */ static projectsForTreeView: ItemForProvider[] = []; /** Contexts sorted and grouped for contexts Tree View */ static contextsForTreeView: ItemForProvider[] = []; /** Comment line ranges */ static commentLines: Range[] = []; /** If active text editor matches `activatePattern` config */ static theRightFileOpened = false; /** Last time file was opened (for resetting completion of recurring tasks) */ static lastVisitByFile: Record<string, Date> = {}; /** Current filter value of tasks Tree View */ static taskTreeViewFilterValue = ''; /** Reference to the extension context for access beyond the `activate()` function */ static extensionContext = {} as any as ExtensionContext; /** Reference to active document. */ static activeDocument: TextDocument | undefined = undefined; /** Used in parsing of nested tasks. */ static activeDocumentTabSize = 4; /** Editor line height (in px) */ static editorLineHeight = 20; } export const enum Constants { ExtensionSettingsPrefix = 'todomd', LastVisitByFileStorageKey = 'LAST_VISIT_BY_FILE_STORAGE_KEY', TagsTreeViewId = 'todomd.tags', ProjectsTreeViewId = 'todomd.projects', ContextsTreeViewId = 'todomd.contexts', DueTreeViewId = 'todomd.due', TasksTreeViewId = 'todomd.tasks', ArchivedTreeViewId = 'todomd.archived', Generic1TreeViewId = 'todomd.generic1', Generic2TreeViewId = 'todomd.generic2', Generic3TreeViewId = 'todomd.generic3', DefaultFileSetting = 'todomd.defaultFile', DefaultArchiveFileSetting = 'todomd.defaultArchiveFile', ExtensionMenuPrefix = 'Todo MD:', ThrottleEverything = 120, } export let $config = workspace.getConfiguration().get(Constants.ExtensionSettingsPrefix) as ExtensionConfig; export const counterStatusBar = new CounterStatusBar(); export const mainStatusBar = new MainStatusBar(); /** * Global vscode variables (mostly disposables) */ export class Global { static webviewProvider: TasksWebviewViewProvider; static tagAutocompleteDisposable: Disposable; static projectAutocompleteDisposable: Disposable; static contextAutocompleteDisposable: Disposable; static generalAutocompleteDisposable: Disposable; static specialTagsAutocompleteDisposable: Disposable; static setDueDateAutocompleteDisposable: Disposable; static hoverDisposable: Disposable; static documentHighlightsDisposable: Disposable; static renameProviderDisposable: Disposable; static referenceProviderDisposable: Disposable; static changeTextDocumentDisposable: Disposable; static changeActiveTextEditorDisposable: Disposable; static completedTaskDecorationType: TextEditorDecorationType; static commentDecorationType: TextEditorDecorationType; static priorityADecorationType: TextEditorDecorationType; static priorityBDecorationType: TextEditorDecorationType; static priorityCDecorationType: TextEditorDecorationType; static priorityDDecorationType: TextEditorDecorationType; static priorityEDecorationType: TextEditorDecorationType; static priorityFDecorationType: TextEditorDecorationType; static tagsDecorationType: TextEditorDecorationType; static tagWithDelimiterDecorationType: TextEditorDecorationType; static tagsDelimiterDecorationType: TextEditorDecorationType; static specialTagDecorationType: TextEditorDecorationType; static projectDecorationType: TextEditorDecorationType; static contextDecorationType: TextEditorDecorationType; static notDueDecorationType: TextEditorDecorationType; static dueDecorationType: TextEditorDecorationType; static overdueDecorationType: TextEditorDecorationType; static invalidDueDateDecorationType: TextEditorDecorationType; static closestDueDateDecorationType: TextEditorDecorationType; static nestedTasksCountDecorationType: TextEditorDecorationType; static nestedTasksPieDecorationType: TextEditorDecorationType; static userSpecifiedAdvancedTagDecorations: boolean; } export async function activate(extensionContext: ExtensionContext) { $state.extensionContext = extensionContext; const lastVisitByFile = extensionContext.globalState.get<typeof $state['lastVisitByFile'] | undefined>(Constants.LastVisitByFileStorageKey); $state.lastVisitByFile = lastVisitByFile ? lastVisitByFile : {}; $state.editorLineHeight = getEditorLineHeight(); updateEditorDecorationStyle(); updateUserSuggestItems(); registerAllCommands(); createAllTreeViews(); const defaultFileDocument = await getDocumentForDefaultFile(); if (defaultFileDocument) { const filePath = defaultFileDocument.uri.toString(); const needReset = checkIfNeedResetRecurringTasks(filePath); if (needReset) { await resetAllRecurringTasks(defaultFileDocument, needReset.lastVisit); await updateLastVisitGlobalState(filePath, new Date()); } } setTimeout(() => { onChangeActiveTextEditor(window.activeTextEditor);// Trigger on change event at activation }); await updateState(); Global.webviewProvider = new TasksWebviewViewProvider($state.extensionContext.extensionUri); $state.extensionContext.subscriptions.push( window.registerWebviewViewProvider(TasksWebviewViewProvider.viewType, Global.webviewProvider), ); updateAllTreeViews(); updateArchivedTasks(); updateIsDevContext(); updateLanguageFeatures(); /** * The event is fired twice quickly when closing an editor, also when swtitching to untitled file ??? */ Global.changeActiveTextEditorDisposable = window.onDidChangeActiveTextEditor(throttle(onChangeActiveTextEditor, 20, { leading: false, })); function onConfigChange(e: ConfigurationChangeEvent) { if (!e.affectsConfiguration(Constants.ExtensionSettingsPrefix)) { return; } updateConfig(); } function updateConfig() { $config = workspace.getConfiguration().get(Constants.ExtensionSettingsPrefix) as ExtensionConfig; disposeEditorDisposables(); updateLanguageFeatures(); $state.editorLineHeight = getEditorLineHeight(); updateEditorDecorationStyle(); updateUserSuggestItems(); mainStatusBar.show(); onChangeActiveTextEditor(window.activeTextEditor); updateIsDevContext(); } function updateIsDevContext() { if (process.env.NODE_ENV === 'development' || $config.isDev) { setContext(VscodeContext.IsDev, true); } } extensionContext.subscriptions.push(workspace.onDidChangeConfiguration(onConfigChange)); } /** * Update primary `state` properties, such as `tasks` or `tags`, based on provided document or based on default file */ export async function updateState() { let document = await getActiveDocument(); if (!document) { document = await getDocumentForDefaultFile(); } if (!document) { $state.tasks = []; $state.tasksAsTree = []; $state.tags = []; $state.projects = []; $state.contexts = []; $state.tagsForTreeView = []; $state.projectsForTreeView = []; $state.contextsForTreeView = []; $state.commentLines = []; $state.theRightFileOpened = false; $state.activeDocument = undefined; return; } const parsedDocument = await parseDocument(document); $state.tasks = parsedDocument.tasks; $state.tasksAsTree = parsedDocument.tasksAsTree; $state.commentLines = parsedDocument.commentLines; const treeItems = groupAndSortTreeItems($state.tasksAsTree); $state.tagsForTreeView = treeItems.tagsForProvider; $state.projectsForTreeView = treeItems.projectsForProvider; $state.contextsForTreeView = treeItems.contextsForProvider; $state.tags = treeItems.tags; $state.projects = treeItems.projects; $state.contexts = treeItems.contexts; } function disposeEditorDisposables() { Global.completedTaskDecorationType?.dispose(); Global.commentDecorationType?.dispose(); Global.priorityADecorationType?.dispose(); Global.priorityBDecorationType?.dispose(); Global.priorityCDecorationType?.dispose(); Global.priorityDDecorationType?.dispose(); Global.priorityEDecorationType?.dispose(); Global.priorityFDecorationType?.dispose(); Global.tagsDecorationType?.dispose(); Global.tagWithDelimiterDecorationType?.dispose(); Global.tagsDelimiterDecorationType?.dispose(); Global.specialTagDecorationType?.dispose(); Global.projectDecorationType?.dispose(); Global.contextDecorationType?.dispose(); Global.notDueDecorationType?.dispose(); Global.dueDecorationType?.dispose(); Global.overdueDecorationType?.dispose(); Global.invalidDueDateDecorationType?.dispose(); Global.closestDueDateDecorationType?.dispose(); Global.nestedTasksCountDecorationType?.dispose(); Global.nestedTasksPieDecorationType?.dispose(); Global.changeTextDocumentDisposable?.dispose(); } /** * Update global storage value of last visit by file */ export async function updateLastVisitGlobalState(stringUri: string, date: Date) { $state.lastVisitByFile[stringUri] = date; await $state.extensionContext.globalState.update(Constants.LastVisitByFileStorageKey, $state.lastVisitByFile); } export function deactivate() { disposeEditorDisposables(); Global.tagAutocompleteDisposable?.dispose(); Global.projectAutocompleteDisposable?.dispose(); Global.contextAutocompleteDisposable?.dispose(); Global.generalAutocompleteDisposable?.dispose(); Global.specialTagsAutocompleteDisposable?.dispose(); Global.setDueDateAutocompleteDisposable?.dispose(); Global.changeTextDocumentDisposable?.dispose(); Global.hoverDisposable?.dispose(); Global.documentHighlightsDisposable?.dispose(); Global.renameProviderDisposable?.dispose(); Global.referenceProviderDisposable?.dispose(); Global.changeActiveTextEditorDisposable?.dispose(); }