Skip to content

Star Problems Extension && All Problems Category #188

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

Closed
wants to merge 10 commits into from
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@
"title": "Show Problem",
"category": "LeetCode"
},
{
"command": "leetcode.toggleFavorite",
"title": "Toggle Favorite Problem",
"category": "LeetCode"
},
{
"command": "leetcode.searchProblem",
"title": "Search Problem",
Expand Down Expand Up @@ -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": [
Expand Down
19 changes: 19 additions & 0 deletions src/commands/star.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
}
}
34 changes: 21 additions & 13 deletions src/explorer/LeetCodeNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
}
}
158 changes: 99 additions & 59 deletions src/explorer/LeetCodeTreeDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,37 @@ import { LeetCodeNode } from "./LeetCodeNode";

export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCodeNode> {

private allProblems: Map<string, IProblem>; // maintains the ownership of all problems.

private treeData: {
Difficulty: Map<string, IProblem[]>,
Tag: Map<string, IProblem[]>,
Company: Map<string, IProblem[]>,
Favorite: IProblem[],
[Category.All]: IProblem[],
[Category.Difficulty]: Map<string, IProblem[]>,
[Category.Tag]: Map<string, IProblem[]>,
[Category.Company]: Map<string, IProblem[]>,
[Category.Favorite]: IProblem[],
};

private onDidChangeTreeDataEvent: vscode.EventEmitter<any> = new vscode.EventEmitter<any>();
private onDidChangeTreeDataEvent: vscode.EventEmitter<LeetCodeNode> = new vscode.EventEmitter<LeetCodeNode>();
Copy link
Member

@jdneo jdneo Mar 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also support refresh the whole explorer, so the parameter type can be written as LeetCodeNode | null | undefined

You can address it in the next PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The event fire's parameter is fire(data: T?) where T=LeetCodeNode, so no need to append null and undefined here

// tslint:disable-next-line:member-ordering
public readonly onDidChangeTreeData: vscode.Event<any> = this.onDidChangeTreeDataEvent.event;
public readonly onDidChangeTreeData: vscode.Event<LeetCodeNode> = this.onDidChangeTreeDataEvent.event;

constructor(private context: vscode.ExtensionContext) { }

public async refresh(): Promise<void> {
await this.getProblemData();
await this.getFullProblemData();
this.onDidChangeTreeDataEvent.fire();
}

public async updateProblem(problem: IProblem): Promise<void> {
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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that the reason that explorer returns to uncollapsed state is that, we do not pass the node element into this.onDidChangeTreeDataEvent.fire() (This API can accept a node element. Then all the data are cleared before the explorer rerendering.

Copy link
Contributor Author

@Vigilans Vigilans Mar 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some investigations I seems to get the hang of this API. Maybe we can keep another reference container here:

allTreeNodes: Map<string, LeetcodeNode[]> // id -> relative nodes

Then, in updateProblem function, we can fire events to request all relative nodes to be updated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Vigilans Cool. Noticed that now the same problem in the different category has different id. So if we refresh the Two Sum in All Problem, will the Two Sum in Difficulty be refreshed?

That means, I'm thinking that if we should make the same problem has the same id in the whole explorer. Not sure the real behavior of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After further investigations, to accomplish this feature, there are some important changes which I think should be discussed in next PR. In this PR, we may temporarily accept the current behavior?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So updateProblem() is not used in this PR, right?

}
}

public getTreeItem(element: LeetCodeNode): vscode.TreeItem | Thenable<vscode.TreeItem> {
if (element.id === "notSignIn") {
if (element.id === "NotSignIn") {
return {
label: element.name,
id: element.id,
Expand All @@ -42,71 +53,75 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod
title: "Sign in to LeetCode",
},
};
} else if (!element.isProblem) { // category
return {
label: element.name,
tooltip: this.getSubCategoryTooltip(element),
id: `LeetCode.Category::${element.parentId}.${element.id}`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have element.parentId, Data.Now() prefix seems no longer necessary.

Btw, the TreeItem Generation code here is separated here for better readability, as well as convenience for next PR(store generated TreeItem somewhere in TreeProvider or LeetCodeNode)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you will send out another PR to remove element.parentId & Data.Now(), right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data.Now() has already been removed here, which is taken place by element.parentId

collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
contextValue: `${element.parentId}.${element.id}`.toLowerCase(),
};
} else { // problem
return {
label: `[${element.id}] ${element.name} ${element.isFavorite ? "♥" : ""}`,
tooltip: "", // TODO: Add Problem Tooltip
id: `LeetCode.Problem::${element.parentId}.${element.id}`,
collapsibleState: vscode.TreeItemCollapsibleState.None,
contextValue: "problem",
iconPath: this.parseIconPathFromProblemState(element),
};
}

const idPrefix: number = Date.now();
return {
label: element.isProblem ? `[${element.id}] ${element.name}` : element.name,
tooltip: this.getSubCategoryTooltip(element),
id: `${idPrefix}.${element.parentName}.${element.id}`,
collapsibleState: element.isProblem ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed,
contextValue: element.isProblem ? "problem" : element.id.toLowerCase(),
iconPath: this.parseIconPathFromProblemState(element),
};
}

public getChildren(element?: LeetCodeNode | undefined): vscode.ProviderResult<LeetCodeNode[]> {
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<void> {
private async getFullProblemData(): Promise<void> {
// clear cache
this.allProblems = new Map<string, IProblem>();
this.treeData = {
Difficulty: new Map<string, IProblem[]>(),
Tag: new Map<string, IProblem[]>(),
Company: new Map<string, IProblem[]>(),
Favorite: [],
[Category.All]: [],
[Category.Difficulty]: new Map<string, IProblem[]>(),
[Category.Tag]: new Map<string, IProblem[]>(),
[Category.Company]: new Map<string, IProblem[]>(),
[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);
Expand All @@ -121,9 +136,9 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod
}

private composeProblemNodes(node: LeetCodeNode): LeetCodeNode[] {
const map: Map<string, IProblem[]> | undefined = this.treeData[node.parentName];
const map: Map<string, IProblem[]> | 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) || [];
Expand All @@ -136,8 +151,8 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod

private composeSubCategoryNodes(node: LeetCodeNode): LeetCodeNode[] {
const category: Category = node.name as Category;
if (category === Category.Favorite) {
leetCodeChannel.appendLine("No sub-level for Favorite nodes");
if (category === Category.All || category === Category.Favorite) {
leetCodeChannel.appendLine("No sub-level for All or Favorite nodes");
return [];
}
const map: Map<string, IProblem[]> | undefined = this.treeData[category];
Expand All @@ -149,7 +164,7 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod
}

private parseIconPathFromProblemState(element: LeetCodeNode): string {
if (!element.isProblem) {
if (!element.isProblem) { // In fact will never be satisfied
return "";
}
switch (element.state) {
Expand All @@ -168,12 +183,23 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod
}

private getSubCategoryTooltip(element: LeetCodeNode): string {
// return '' unless it is a sub-category node
if (element.isProblem || !this.treeData[element.parentName]) {
// return '' if it does not directly hold problems.
if (element.isProblem) { // In fact will never be satisfied
return "";
}

const problems: IProblem[] = this.treeData[element.parentName].get(element.id);
let problems: IProblem[];
switch (element.name) {
case Category.Difficulty:
case Category.Tag:
case Category.Company:
return "";
case Category.All:
case Category.Favorite:
problems = this.treeData[element.name];
break;
default:
problems = this.treeData[element.parentId].get(element.id);
}

let acceptedNum: number = 0;
let failedNum: number = 0;
Expand All @@ -197,13 +223,27 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider<LeetCod
].join(os.EOL);
}

private updateTreeDataByProblem(problem: IProblem): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method name her is confusing. Cuz we only handle isFavorite here, but the method name looks like we can update all the fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we might handle the case of updating other treeData fields the at a later time. Seems a overdesign here?
Should I leave a comment announcing modification of other categories is not needed yet here, or just change the method name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure now other fields will be updated here in the near PRs, e.g. when a problem is accepted, there will be some code here to update other fields.

const origin: IProblem | undefined = this.allProblems.get(problem.id);
if (origin && origin.isFavorite !== problem.isFavorite) {
// Find appropriate index to insert/delete a problem
const problemIndex: number = this.treeData[Category.Favorite].findIndex((p: LeetCodeNode) => 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);
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -44,6 +45,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
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)),
Expand Down
10 changes: 10 additions & 0 deletions src/leetCodeExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ class LeetCodeExecutor {
return filePath;
}

public async toggleFavorite(node: IProblem, markStarred: boolean): Promise<boolean> {
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<string> {
return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "session"]);
}
Expand Down
Loading