diff --git a/src/extension.ts b/src/extension.ts index 6d31417..9f30a1a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,10 +1,14 @@ import { ExtensionContext } from 'vscode' import { registerTrees } from './tree/registerTrees' import { registerCommands } from './commands/registerCommands' +import ServiceLocator from './tree/services/ServiceLocator'; +import { ISyncService, SyncService } from './tree/services/SyncService' export function activate(context: ExtensionContext) { + ServiceLocator.register('SyncService', new SyncService(context)); + registerTrees(context) registerCommands(context) } -export function deactivate() {} +export function deactivate() { } diff --git a/src/tree/questions/QuestionItem.ts b/src/tree/questions/QuestionItem.ts index 2edc879..a4f8280 100644 --- a/src/tree/questions/QuestionItem.ts +++ b/src/tree/questions/QuestionItem.ts @@ -1,14 +1,34 @@ import { Command, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode' export class QuestionItem extends TreeItem { - constructor( - public readonly label: string, - public readonly command?: Command, - public readonly iconPath?: string | ThemeIcon, - public readonly tooltip?: string, - public readonly collapsibleState?: TreeItemCollapsibleState - ) { + public readonly label: string + public readonly command?: Command + public readonly iconPath?: string | ThemeIcon + public readonly tooltip?: string + public readonly collapsibleState?: TreeItemCollapsibleState + public readonly description?: string | boolean + constructor({ + label, + command, + iconPath, + tooltip, + collapsibleState, + description + }: { + label: string; + command?: Command; + iconPath?: string | ThemeIcon; + tooltip?: string; + collapsibleState?: TreeItemCollapsibleState; + description?: string | boolean; + }) { super(label) + this.label = label + this.command = command + this.iconPath = iconPath + this.tooltip = tooltip + this.collapsibleState = collapsibleState + this.description = description } contextValue = 'question' diff --git a/src/tree/questions/QuestionsProvider.ts b/src/tree/questions/QuestionsProvider.ts index 0e05824..e5aeb85 100644 --- a/src/tree/questions/QuestionsProvider.ts +++ b/src/tree/questions/QuestionsProvider.ts @@ -12,7 +12,8 @@ import { Question, TagMetaInfo, AuthorMetaInfo, - DifficultyMetaInfo + DifficultyMetaInfo, + QuestionStatus } from '../../types' export class QuestionsProvider implements TreeDataProvider { @@ -138,15 +139,17 @@ export class QuestionsProvider implements TreeDataProvider { genQuestionsItems(questions: Question[]): QuestionItem[] { const questionItems: QuestionItem[] = [] questions.forEach((question) => { - const treeItem = new QuestionItem( - `${question.idx!} - ${question.title!}`, - { + const treeItem = new QuestionItem({ + label: `${question.idx!} - ${question.title!}`, + command: { title: 'Preview Question', command: Commands.PreviewQuestion, - arguments: [question] + arguments: [question], }, - this.getStatusIcon(question._status) - ) + iconPath: this.getStatusIcon(question._status), + description: question._status === 'completeOnRemote' ? 'remote' : undefined, + }) + questionItems.push(treeItem) }) return questionItems @@ -158,6 +161,7 @@ export class QuestionsProvider implements TreeDataProvider { const todoIconPath = path.join(__dirname, '..', '..', '..', 'resources', 'todo.svg') switch (status) { case 'complete': + case 'completeOnRemote': return completeIconPath case 'error': return errorIconPath @@ -192,20 +196,20 @@ export class QuestionsProvider implements TreeDataProvider { } getFinishedLengthOfAllQuestions(): number { - const finishedLength = this.allQuestions.filter((item) => item._status === 'complete').length + const finishedLength = this.allQuestions.filter((item) => isCompleted(item._status)).length return finishedLength } getFinishedLengthOfDifficulty(difficulty: string): number { const finishedLength = this.allQuestions.filter( - (item) => item.difficulty === difficulty.toLocaleLowerCase() && item._status === 'complete' + (item) => item.difficulty === difficulty.toLocaleLowerCase() && isCompleted(item._status) ).length return finishedLength } getFinishedLengthOfTag(tag: string): number { const finishedLength = this.allQuestions.filter( - (item) => item.info?.tags?.includes?.(tag) && item._status === 'complete' + (item) => item.info?.tags?.includes?.(tag) && isCompleted(item._status) ).length return finishedLength } @@ -214,8 +218,12 @@ export class QuestionsProvider implements TreeDataProvider { const finishedLength = this.allQuestions.filter( (item) => (item.info?.author?.name === author || item.info?.author?.github === author) && - item._status === 'complete' + isCompleted(item._status) ).length return finishedLength } } + +function isCompleted(status?: QuestionStatus): boolean { + return status === 'complete' || status === 'completeOnRemote' +} \ No newline at end of file diff --git a/src/tree/registerTrees.ts b/src/tree/registerTrees.ts index c588c08..d0caf86 100644 --- a/src/tree/registerTrees.ts +++ b/src/tree/registerTrees.ts @@ -26,17 +26,19 @@ export async function registerTrees(context: ExtensionContext): Promise { context.subscriptions.push( workspace.onDidSaveTextDocument(() => { const editor = window.activeTextEditor - if (editor) { - const workspaceFolderSetting = getWorkspaceFolder() - if ( - !workspaceFolderSetting || - !fs.existsSync(workspaceFolderSetting) || - !editor.document.fileName.startsWith(workspaceFolderSetting) - ) { - return - } - questionsProvider.refresh() + if (!editor) { + return } + + const workspaceFolderSetting = getWorkspaceFolder() + if ( + !workspaceFolderSetting || + !fs.existsSync(workspaceFolderSetting) || + !editor.document.fileName.startsWith(workspaceFolderSetting) + ) { + return + } + questionsProvider.refresh() }) ) window.registerTreeDataProvider('typeChallenges.questions', questionsProvider) diff --git a/src/tree/services/ServiceLocator.ts b/src/tree/services/ServiceLocator.ts new file mode 100644 index 0000000..11ad74c --- /dev/null +++ b/src/tree/services/ServiceLocator.ts @@ -0,0 +1,17 @@ +class ServiceLocator { + private static services: Map = new Map(); + + static register(name: string, service: T): void { + ServiceLocator.services.set(name, service); + } + + static get(name: string): T { + const service = ServiceLocator.services.get(name); + if (!service) { + throw new Error(`Service ${name} not found`); + } + return service; + } +} + +export default ServiceLocator; diff --git a/src/tree/services/SyncService.ts b/src/tree/services/SyncService.ts new file mode 100644 index 0000000..aaf78e5 --- /dev/null +++ b/src/tree/services/SyncService.ts @@ -0,0 +1,53 @@ +import { ExtensionContext } from "vscode"; +import { CompletedQuestionIds, CompletedQuestionIdsSync, SyncState } from "./types"; +var os = require("os"); + + +export interface ISyncService { + putCompletedQuestions(localCompletedIds: string[]): void + getCompletedQuestions(): CompletedQuestionIdsSync +} + +const root = 'root' +export class SyncService implements ISyncService { + context: ExtensionContext; + hostName: string; + + constructor(context: ExtensionContext) { + this.context = context + this.hostName = `${os.hostname()}`; + + this.context.globalState.setKeysForSync([root]); // sync state for all machines + } + + private _getState(): SyncState { + return this.context.globalState.get(root) || {} + } + + getCompletedQuestions(): CompletedQuestionIdsSync { + const statePerHost = this._getState(); + + const idsLocal = new Set() + const idsRemote = new Set() + + Object + .keys(statePerHost) + .forEach((hostName) => { + if (hostName === this.hostName) { + statePerHost[hostName].forEach((completedId: string) => idsLocal.add(completedId)) + } else { + statePerHost[hostName].forEach((completedId: string) => idsRemote.add(completedId)) + } + },) + + return { + local: Array.from(idsLocal), + remote: Array.from(idsRemote), + } + } + + putCompletedQuestions(completed: string[]): void { + this.context.globalState + .update(root, { ...this._getState(), [this.hostName]: completed }) + } +} \ No newline at end of file diff --git a/src/tree/services/types.ts b/src/tree/services/types.ts new file mode 100644 index 0000000..a548fd5 --- /dev/null +++ b/src/tree/services/types.ts @@ -0,0 +1,10 @@ +export type CompletedQuestionIds = string[] + +export type SyncState = { + [HostId in string]: CompletedQuestionIds +} + +export type CompletedQuestionIdsSync = { + local: CompletedQuestionIds, + remote: CompletedQuestionIds, +} diff --git a/src/types.ts b/src/types.ts index b90c6c7..0097838 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,8 @@ export enum Difficulty { Extreme = 'Extreme' } +export type QuestionStatus = 'complete' | 'error' | 'todo' | 'completeOnRemote' + export interface Question { idx?: number title?: string @@ -26,7 +28,7 @@ export interface Question { template?: string testCases?: string _original?: string - _status?: 'complete' | 'error' | 'todo' + _status?: QuestionStatus } export interface QuestionMetaInfo { diff --git a/src/utils/questions.ts b/src/utils/questions.ts index b46df78..187c996 100644 --- a/src/utils/questions.ts +++ b/src/utils/questions.ts @@ -6,11 +6,32 @@ import * as fse from 'fs-extra' import { window } from 'vscode' import { AuthorMetaInfo, Difficulty, DifficultyMetaInfo, ExecError, Question, TagMetaInfo } from '../types' import { getWorkspaceFolder } from './settings' +import { ISyncService } from '../tree/services/SyncService' +import ServiceLocator from '../tree/services/ServiceLocator' const rootPath = path.join(__dirname, '..', '..', 'resources', 'questions') const tsConfigFileName = 'tsconfig.json' export async function getAllQuestions(): Promise { + const allQuestions = await _getAllQuestions(); + + const syncService = ServiceLocator.get('SyncService'); + + syncService.putCompletedQuestions(_getCompletedIds(allQuestions)) + + const completedQuestionIds = syncService.getCompletedQuestions() + + allQuestions.forEach((question) => { + if (question.idx && completedQuestionIds.remote.includes(question.idx.toString())) { + question._status = 'completeOnRemote' + } + }) + + return allQuestions; +} + + +async function _getAllQuestions(): Promise { await createTsConfigFile() const localQuestions = getLocalQuestions() const localErrorQuestions = await getLocalErrorQuestions() @@ -80,6 +101,13 @@ export async function getAllQuestions(): Promise { return result } +function _getCompletedIds(allQuestions: Question[]) { + return allQuestions + .filter((question) => question._status === 'complete') + .map(question => question.idx?.toString()) + .filter((id): id is string => !!id) +} + export function getAllTags(questions: Question[]): string[] { const set = new Set() for (const q of questions) {