diff --git a/package.json b/package.json index 8a316d76..eb9d04b8 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,11 @@ "title": "Show Problem", "category": "LeetCode" }, + { + "command": "leetcode.toggleFavorite", + "title": "Toggle Favorite Problem", + "category": "LeetCode" + }, { "command": "leetcode.searchProblem", "title": "Search Problem", @@ -164,12 +169,21 @@ "command": "leetcode.showProblem", "when": "view == leetCodeExplorer && viewItem == problem", "group": "leetcode@1" + }, + { + "command": "leetcode.toggleFavorite", + "when": "view == leetCodeExplorer && viewItem == problem", + "group": "leetcode@1" } ], "commandPalette": [ { "command": "leetcode.showProblem", "when": "never" + }, + { + "command": "leetcode.toggleFavorite", + "when": "never" } ], "explorer/context": [ diff --git a/src/commands/star.ts b/src/commands/star.ts new file mode 100644 index 00000000..e1db10da --- /dev/null +++ b/src/commands/star.ts @@ -0,0 +1,19 @@ +// Copyright (c) jdneo. All rights reserved. +// Licensed under the MIT license. + +import { LeetCodeNode } from "../explorer/LeetCodeNode"; +import { LeetCodeTreeDataProvider } from "../explorer/LeetCodeTreeDataProvider"; +import { leetCodeExecutor } from "../leetCodeExecutor"; +import { IProblem } from "../shared"; +import { DialogType, promptForOpenOutputChannel } from "../utils/uiUtils"; + +export async function toggleFavorite(provider: LeetCodeTreeDataProvider, node: LeetCodeNode): Promise { + try { + const problem: IProblem = Object.assign({}, node.nodeData, { + isFavorite: await leetCodeExecutor.toggleFavorite(node, !node.isFavorite), + }); + provider.updateProblem(problem); + } catch (error) { + await promptForOpenOutputChannel("Failed to star the problem. Please open the output channel for details.", DialogType.error); + } +} diff --git a/src/explorer/LeetCodeNode.ts b/src/explorer/LeetCodeNode.ts index ad5211cb..3001903a 100644 --- a/src/explorer/LeetCodeNode.ts +++ b/src/explorer/LeetCodeNode.ts @@ -4,19 +4,31 @@ import { IProblem, ProblemState } from "../shared"; export class LeetCodeNode { - constructor(private data: IProblem, private parentNodeName: string, private isProblemNode: boolean = true) { } + constructor( + private data: IProblem, + private parentNodeId: string, + private isProblemNode: boolean = true) { } + + public get nodeData(): IProblem { + return this.data; + } + + public get isProblem(): boolean { + return this.isProblemNode; + } + + public get parentId(): string { + return this.parentNodeId; + } public get locked(): boolean { return this.data.locked; } + public get name(): string { return this.data.name; } - public get state(): ProblemState { - return this.data.state; - } - public get id(): string { return this.data.id; } @@ -37,15 +49,11 @@ export class LeetCodeNode { return this.data.companies; } - public get isFavorite(): boolean { - return this.data.isFavorite; - } - - public get isProblem(): boolean { - return this.isProblemNode; + public get state(): ProblemState { + return this.data.state; } - public get parentName(): string { - return this.parentNodeName; + public get isFavorite(): boolean { + return this.data.isFavorite; } } diff --git a/src/explorer/LeetCodeTreeDataProvider.ts b/src/explorer/LeetCodeTreeDataProvider.ts index da9ca751..db93080d 100644 --- a/src/explorer/LeetCodeTreeDataProvider.ts +++ b/src/explorer/LeetCodeTreeDataProvider.ts @@ -13,26 +13,37 @@ import { LeetCodeNode } from "./LeetCodeNode"; export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider { + private allProblems: Map; // maintains the ownership of all problems. + private treeData: { - Difficulty: Map, - Tag: Map, - Company: Map, - Favorite: IProblem[], + [Category.All]: IProblem[], + [Category.Difficulty]: Map, + [Category.Tag]: Map, + [Category.Company]: Map, + [Category.Favorite]: IProblem[], }; - private onDidChangeTreeDataEvent: vscode.EventEmitter = new vscode.EventEmitter(); + private onDidChangeTreeDataEvent: vscode.EventEmitter = new vscode.EventEmitter(); // tslint:disable-next-line:member-ordering - public readonly onDidChangeTreeData: vscode.Event = this.onDidChangeTreeDataEvent.event; + public readonly onDidChangeTreeData: vscode.Event = this.onDidChangeTreeDataEvent.event; constructor(private context: vscode.ExtensionContext) { } public async refresh(): Promise { - await this.getProblemData(); + await this.getFullProblemData(); this.onDidChangeTreeDataEvent.fire(); } + public async updateProblem(problem: IProblem): Promise { + if (this.allProblems.has(problem.id)) { + this.updateTreeDataByProblem(problem); // only modify the content of tree data, problem is not updated. + Object.assign(this.allProblems.get(problem.id), problem); // update problem, where reference is preserved. + this.onDidChangeTreeDataEvent.fire(); + } + } + public getTreeItem(element: LeetCodeNode): vscode.TreeItem | Thenable { - if (element.id === "notSignIn") { + if (element.id === "NotSignIn") { return { label: element.name, id: element.id, @@ -42,71 +53,75 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider { if (!leetCodeManager.getUser()) { return [ new LeetCodeNode(Object.assign({}, defaultProblem, { - id: "notSignIn", + id: "NotSignIn", name: "Sign in to LeetCode", - }), "ROOT", false), + }), "Root", false), ]; } if (!element) { // Root view return [ - new LeetCodeNode(Object.assign({}, defaultProblem, { - id: Category.Difficulty, - name: Category.Difficulty, - }), "ROOT", false), - new LeetCodeNode(Object.assign({}, defaultProblem, { - id: Category.Tag, - name: Category.Tag, - }), "ROOT", false), - new LeetCodeNode(Object.assign({}, defaultProblem, { - id: Category.Company, - name: Category.Company, - }), "ROOT", false), - new LeetCodeNode(Object.assign({}, defaultProblem, { - id: Category.Favorite, - name: Category.Favorite, - }), "ROOT", false), - ]; + Category.All, + Category.Difficulty, + Category.Tag, + Category.Company, + Category.Favorite, + ].map((c: Category) => new LeetCodeNode( + Object.assign({}, defaultProblem, { id: c, name: c }), "Root", false, + )); } else { - switch (element.name) { // First-level + // First-level + switch (element.name) { + case Category.All: case Category.Favorite: - const nodes: IProblem[] = this.treeData[Category.Favorite]; - return nodes.map((p: IProblem) => new LeetCodeNode(p, Category.Favorite)); + const nodes: IProblem[] = this.treeData[element.name]; + return nodes.map((p: IProblem) => new LeetCodeNode(p, element.name)); case Category.Difficulty: case Category.Tag: case Category.Company: return this.composeSubCategoryNodes(element); - default: // Second and lower levels - return element.isProblem ? [] : this.composeProblemNodes(element); } + // Second and lower levels + return element.isProblem ? [] : this.composeProblemNodes(element); } } - private async getProblemData(): Promise { + private async getFullProblemData(): Promise { // clear cache + this.allProblems = new Map(); this.treeData = { - Difficulty: new Map(), - Tag: new Map(), - Company: new Map(), - Favorite: [], + [Category.All]: [], + [Category.Difficulty]: new Map(), + [Category.Tag]: new Map(), + [Category.Company]: new Map(), + [Category.Favorite]: [], }; for (const problem of await list.listProblems()) { + // Add every problem to problem pool + this.allProblems.set(problem.id, problem); // Add favorite problem, no matter whether it is solved. if (problem.isFavorite) { this.treeData[Category.Favorite].push(problem); @@ -121,9 +136,9 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider | undefined = this.treeData[node.parentName]; + const map: Map | undefined = this.treeData[node.parentId]; if (!map) { - leetCodeChannel.appendLine(`Category: ${node.parentName} is not available.`); + leetCodeChannel.appendLine(`Category: ${node.parentId} is not available.`); return []; } const problems: IProblem[] = map.get(node.name) || []; @@ -136,8 +151,8 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider | undefined = this.treeData[category]; @@ -149,7 +164,7 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider Number(p.id) >= Number(problem.id)); + if (problem.isFavorite) { + this.treeData[Category.Favorite].splice(problemIndex, 0, origin); // insert original problem's reference as favorite + } else { + this.treeData[Category.Favorite].splice(problemIndex, 1); // delete favorite + } + } + } + private addProblemToTreeData(problem: IProblem): void { - this.putProblemToMap(this.treeData.Difficulty, problem.difficulty, problem); + this.treeData[Category.All].push(problem); + this.putProblemToMap(this.treeData[Category.Difficulty], problem.difficulty, problem); for (const tag of problem.tags) { - this.putProblemToMap(this.treeData.Tag, this.beautifyCategoryName(tag), problem); + this.putProblemToMap(this.treeData[Category.Tag], this.beautifyCategoryName(tag), problem); } for (const company of problem.companies) { - this.putProblemToMap(this.treeData.Company, this.beautifyCategoryName(company), problem); + this.putProblemToMap(this.treeData[Category.Company], this.beautifyCategoryName(company), problem); } } diff --git a/src/extension.ts b/src/extension.ts index aaf2ee4f..e5c110bb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,6 +8,7 @@ import { switchDefaultLanguage } from "./commands/language"; import * as plugin from "./commands/plugin"; import * as session from "./commands/session"; import * as show from "./commands/show"; +import * as star from "./commands/star"; import * as submit from "./commands/submit"; import * as test from "./commands/test"; import { LeetCodeNode } from "./explorer/LeetCodeNode"; @@ -44,6 +45,7 @@ export async function activate(context: vscode.ExtensionContext): Promise vscode.commands.registerCommand("leetcode.selectSessions", () => session.selectSession()), vscode.commands.registerCommand("leetcode.createSession", () => session.createSession()), vscode.commands.registerCommand("leetcode.showProblem", (node: LeetCodeNode) => show.showProblem(node)), + vscode.commands.registerCommand("leetcode.toggleFavorite", (node: LeetCodeNode) => star.toggleFavorite(leetCodeTreeDataProvider, node)), vscode.commands.registerCommand("leetcode.searchProblem", () => show.searchProblem()), vscode.commands.registerCommand("leetcode.refreshExplorer", () => leetCodeTreeDataProvider.refresh()), vscode.commands.registerCommand("leetcode.testSolution", (uri?: vscode.Uri) => test.testSolution(uri)), diff --git a/src/leetCodeExecutor.ts b/src/leetCodeExecutor.ts index 373bda11..e9f0ae4c 100644 --- a/src/leetCodeExecutor.ts +++ b/src/leetCodeExecutor.ts @@ -87,6 +87,16 @@ class LeetCodeExecutor { return filePath; } + public async toggleFavorite(node: IProblem, markStarred: boolean): Promise { + let description: string = ""; + if (markStarred) { + description = await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "star", node.id]); + } else { + description = await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "star", node.id, "-d"]); + } + return description.includes("♥"); + } + public async listSessions(): Promise { return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "session"]); } diff --git a/src/shared.ts b/src/shared.ts index 942e5ba1..0f84f3af 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -86,6 +86,7 @@ export const defaultProblem: IProblem = { }; export enum Category { + All = "All Problems", Difficulty = "Difficulty", Tag = "Tag", Company = "Company",