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

added sync for completed questions #26

Open
wants to merge 1 commit 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
6 changes: 5 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
@@ -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<ISyncService>('SyncService', new SyncService(context));

registerTrees(context)
registerCommands(context)
}

export function deactivate() {}
export function deactivate() { }
34 changes: 27 additions & 7 deletions src/tree/questions/QuestionItem.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
30 changes: 19 additions & 11 deletions src/tree/questions/QuestionsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
Question,
TagMetaInfo,
AuthorMetaInfo,
DifficultyMetaInfo
DifficultyMetaInfo,
QuestionStatus
} from '../../types'

export class QuestionsProvider implements TreeDataProvider<QuestionItem> {
Expand Down Expand Up @@ -138,15 +139,17 @@ export class QuestionsProvider implements TreeDataProvider<QuestionItem> {
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
Expand All @@ -158,6 +161,7 @@ export class QuestionsProvider implements TreeDataProvider<QuestionItem> {
const todoIconPath = path.join(__dirname, '..', '..', '..', 'resources', 'todo.svg')
switch (status) {
case 'complete':
case 'completeOnRemote':
return completeIconPath
case 'error':
return errorIconPath
Expand Down Expand Up @@ -192,20 +196,20 @@ export class QuestionsProvider implements TreeDataProvider<QuestionItem> {
}

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
}
Expand All @@ -214,8 +218,12 @@ export class QuestionsProvider implements TreeDataProvider<QuestionItem> {
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'
}
22 changes: 12 additions & 10 deletions src/tree/registerTrees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,19 @@ export async function registerTrees(context: ExtensionContext): Promise<void> {
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)
Expand Down
17 changes: 17 additions & 0 deletions src/tree/services/ServiceLocator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
class ServiceLocator {
private static services: Map<string, any> = new Map();

static register<T>(name: string, service: T): void {
ServiceLocator.services.set(name, service);
}

static get<T>(name: string): T {
const service = ServiceLocator.services.get(name);
if (!service) {
throw new Error(`Service ${name} not found`);
}
return service;
}
}

export default ServiceLocator;
53 changes: 53 additions & 0 deletions src/tree/services/SyncService.ts
Original file line number Diff line number Diff line change
@@ -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<SyncState>(root) || {}
}

getCompletedQuestions(): CompletedQuestionIdsSync {
const statePerHost = this._getState();

const idsLocal = new Set<string>()
const idsRemote = new Set<string>()

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 })
}
}
10 changes: 10 additions & 0 deletions src/tree/services/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type CompletedQuestionIds = string[]

export type SyncState = {
[HostId in string]: CompletedQuestionIds
}

export type CompletedQuestionIdsSync = {
local: CompletedQuestionIds,
remote: CompletedQuestionIds,
}
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export enum Difficulty {
Extreme = 'Extreme'
}

export type QuestionStatus = 'complete' | 'error' | 'todo' | 'completeOnRemote'

export interface Question {
idx?: number
title?: string
Expand All @@ -26,7 +28,7 @@ export interface Question {
template?: string
testCases?: string
_original?: string
_status?: 'complete' | 'error' | 'todo'
_status?: QuestionStatus
}

export interface QuestionMetaInfo {
Expand Down
28 changes: 28 additions & 0 deletions src/utils/questions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Question[]> {
const allQuestions = await _getAllQuestions();

const syncService = ServiceLocator.get<ISyncService>('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<Question[]> {
await createTsConfigFile()
const localQuestions = getLocalQuestions()
const localErrorQuestions = await getLocalErrorQuestions()
Expand Down Expand Up @@ -80,6 +101,13 @@ export async function getAllQuestions(): Promise<Question[]> {
return result
}

function _getCompletedIds(allQuestions: Question[]) {
return allQuestions
.filter((question) => question._status === 'complete')
.map(question => question.idx?.toString())
.filter<string>((id): id is string => !!id)
}

export function getAllTags(questions: Question[]): string[] {
const set = new Set<string>()
for (const q of questions) {
Expand Down