From e90b44e63d6c39c036b5cf59caa320422786f369 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Tue, 22 Jan 2019 20:20:00 +0800 Subject: [PATCH 01/14] Add company plugin check --- src/leetCodeExecutor.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/leetCodeExecutor.ts b/src/leetCodeExecutor.ts index e6356a83..4165e164 100644 --- a/src/leetCodeExecutor.ts +++ b/src/leetCodeExecutor.ts @@ -31,7 +31,6 @@ class LeetCodeExecutor { public async meetRequirements(): Promise { try { await this.executeCommandEx("node", ["-v"]); - return true; } catch (error) { const choice: vscode.MessageItem | undefined = await vscode.window.showErrorMessage( "LeetCode extension needs Node.js installed in environment path", @@ -42,6 +41,12 @@ class LeetCodeExecutor { } return false; } + try { // Check company plugin + await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-e", "company"]); + } catch (error) { // Download company plugin and activate + await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-i", "company"]); + } + return true; } public async deleteCache(): Promise { From c1a58b2e4ea0350a5dac65b0c2705ced67126700 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Tue, 22 Jan 2019 20:33:51 +0800 Subject: [PATCH 02/14] Store leetcode-cli root path in LeetCodeExecutor instead of binary path --- src/leetCodeExecutor.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/leetCodeExecutor.ts b/src/leetCodeExecutor.ts index 4165e164..d48b848d 100644 --- a/src/leetCodeExecutor.ts +++ b/src/leetCodeExecutor.ts @@ -10,22 +10,26 @@ import { DialogOptions, openUrl } from "./utils/uiUtils"; import * as wsl from "./utils/wslUtils"; class LeetCodeExecutor { - private leetCodeBinaryPath: string; - private leetCodeBinaryPathInWsl: string; + private leetCodeRootPath: string; + private leetCodeRootPathInWsl: string; constructor() { - this.leetCodeBinaryPath = path.join(__dirname, "..", "..", "node_modules", "leetcode-cli", "bin", "leetcode"); - this.leetCodeBinaryPathInWsl = ""; + this.leetCodeRootPath = path.join(__dirname, "..", "..", "node_modules", "leetcode-cli"); + this.leetCodeRootPathInWsl = ""; } - public async getLeetCodeBinaryPath(): Promise { + public async getLeetCodeRootPath(): Promise { if (wsl.useWsl()) { - if (!this.leetCodeBinaryPathInWsl) { - this.leetCodeBinaryPathInWsl = `${await wsl.toWslPath(this.leetCodeBinaryPath)}`; + if (!this.leetCodeRootPathInWsl) { + this.leetCodeRootPathInWsl = `${await wsl.toWslPath(this.leetCodeRootPath)}`; } - return `"${this.leetCodeBinaryPathInWsl}"`; + return `"${this.leetCodeRootPathInWsl}"`; } - return `"${this.leetCodeBinaryPath}"`; + return `"${this.leetCodeRootPath}"`; + } + + public async getLeetCodeBinaryPath(): Promise { + return path.join(await this.getLeetCodeRootPath(), "bin", "leetcode"); } public async meetRequirements(): Promise { From ce204b9875201b935de75f58576238d6d90e80ac Mon Sep 17 00:00:00 2001 From: Vigilans Date: Tue, 22 Jan 2019 21:03:19 +0800 Subject: [PATCH 03/14] Add companies and tags information to IProblem (from direct import of leetcode-cli company plugin) --- src/commands/list.ts | 15 ++++++++++++++- src/leetCodeExplorer.ts | 4 ++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/commands/list.ts b/src/commands/list.ts index 62295b4c..c8411806 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import * as vscode from "vscode"; +import * as path from "path"; import { leetCodeExecutor } from "../leetCodeExecutor"; import { leetCodeManager } from "../leetCodeManager"; import { ProblemState, UserStatus } from "../shared"; @@ -15,6 +16,8 @@ export interface IProblem { name: string; difficulty: string; passRate: string; + tags: string[]; + companies: string[]; } export async function listProblems(): Promise { @@ -28,17 +31,21 @@ export async function listProblems(): Promise { const problems: IProblem[] = []; const lines: string[] = result.split("\n"); const reg: RegExp = /^(.)\s(.{1,2})\s(.)\s\[\s*(\d*)\s*\]\s*(.*)\s*(Easy|Medium|Hard)\s*\((\s*\d+\.\d+ %)\)/; + const { companies, tags } = await getCompaniesAndTags(); for (const line of lines) { const match: RegExpMatchArray | null = line.match(reg); if (match && match.length === 8) { + const id = match[4].trim(); problems.push({ favorite: match[1].trim().length > 0, locked: match[2].trim().length > 0, state: parseProblemState(match[3]), - id: match[4].trim(), + id: id, name: match[5].trim(), difficulty: match[6].trim(), passRate: match[7].trim(), + companies: companies[id], + tags: tags[id] }); } } @@ -66,3 +73,9 @@ function parseProblemState(stateOutput: string): ProblemState { return ProblemState.Unknown; } } + +async function getCompaniesAndTags(): Promise<{ companies: { [key: string]: string[] }, tags: { [key: string]: string[] } }> { + const COMPONIES_TAGS_PATH = path.join(await leetCodeExecutor.getLeetCodeRootPath(), "lib", "plugins", "company.js"); + const { COMPONIES, TAGS } = require(COMPONIES_TAGS_PATH); + return { companies: COMPONIES, tags: TAGS }; +} diff --git a/src/leetCodeExplorer.ts b/src/leetCodeExplorer.ts index ecac75fd..efa86ea8 100644 --- a/src/leetCodeExplorer.ts +++ b/src/leetCodeExplorer.ts @@ -85,6 +85,8 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider Date: Tue, 22 Jan 2019 22:13:16 +0800 Subject: [PATCH 04/14] Add category tree view to explorer --- src/commands/list.ts | 22 ++++--- src/leetCodeExplorer.ts | 138 +++++++++++++++++++++++----------------- 2 files changed, 92 insertions(+), 68 deletions(-) diff --git a/src/commands/list.ts b/src/commands/list.ts index c8411806..89e40ff3 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -8,18 +8,20 @@ import { leetCodeManager } from "../leetCodeManager"; import { ProblemState, UserStatus } from "../shared"; import { DialogType, promptForOpenOutputChannel } from "../utils/uiUtils"; -export interface IProblem { - favorite: boolean; - locked: boolean; - state: ProblemState; - id: string; - name: string; - difficulty: string; - passRate: string; - tags: string[]; - companies: string[]; +export const IProblemDefault = { + favorite: false, + locked: false, + state: ProblemState.Unknown, + id: "", + name: "", + difficulty: "", + passRate: "", + companies: [] as string[], + tags: [] as string[] } +export type IProblem = typeof IProblemDefault; + export async function listProblems(): Promise { try { if (leetCodeManager.getStatus() === UserStatus.SignedOut) { diff --git a/src/leetCodeExplorer.ts b/src/leetCodeExplorer.ts index efa86ea8..6ff7b0d4 100644 --- a/src/leetCodeExplorer.ts +++ b/src/leetCodeExplorer.ts @@ -7,6 +7,8 @@ import * as list from "./commands/list"; import { leetCodeManager } from "./leetCodeManager"; import { ProblemState } from "./shared"; +export type Category = "Difficulty" | "Tag" | "Company"; + // tslint:disable:max-classes-per-file export class LeetCodeNode { constructor(private data: list.IProblem, private isProblemNode: boolean = true) { } @@ -37,7 +39,11 @@ export class LeetCodeNode { export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider { - private treeData: Map = new Map(); + private treeData: { + Difficulty: Map, + Tag: Map, + Company: Map + } private onDidChangeTreeDataEvent: vscode.EventEmitter = new vscode.EventEmitter(); // tslint:disable-next-line:member-ordering @@ -77,46 +83,72 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider void): Promise => { await this.getProblemData(); - resolve(this.composeDifficultyNodes()); + resolve([ + new LeetCodeNode(Object.assign(list.IProblemDefault, { + id: "Root", + name: "Difficulty", + }), false), + new LeetCodeNode(Object.assign(list.IProblemDefault, { + id: "Root", + name: "Tag", + }), false), + new LeetCodeNode(Object.assign(list.IProblemDefault, { + id: "Root", + name: "Company", + }), false) + ]); }); } else { - return element.isProblem ? [] : this.composeProblemNodes(element.name); + switch (element.name) { // First-level + case "Difficulty": + case "Tag": + case "Company": + return this.composeCategoryNodes(element); + default: // Second and lower levels + return element.isProblem ? [] : this.composeProblemNodes(element); + } } } private async getProblemData(): Promise { - const allProblems: list.IProblem[] = await list.listProblems(); - this.treeData.clear(); - for (const problem of allProblems) { - const problems: list.IProblem[] | undefined = this.treeData.get(problem.difficulty); - if (problems) { - problems.push(problem); - } else { - this.treeData.set(problem.difficulty, [problem]); + this.treeData = { + Difficulty: new Map(), + Tag: new Map(), + Company: new Map() + } + for (const problem of await list.listProblems()) { + const categories = [ + ["Difficulty", [problem.difficulty]], + ["Tag", problem.tags], + ["Company", problem.companies] + ] as [Category, string[]][]; + for (const [parent, children] of categories) { + for (const subCategory of children) { + const problems = this.treeData[parent].get(subCategory); + if (problems) { + problems.push(problem); + } else { + this.treeData.Difficulty.set(subCategory, [problem]); + } + } } } } - private composeProblemNodes(difficulty: string): LeetCodeNode[] { - const problems: list.IProblem[] | undefined = this.treeData.get(difficulty); + private composeProblemNodes(node: LeetCodeNode): LeetCodeNode[] { + // node.id stores the parent node name, node.name stores current node name. + const problems: list.IProblem[] | undefined = this.treeData[node.id].get(node.name); if (!problems || problems.length === 0) { return []; } @@ -127,42 +159,32 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider { - function getValue(input: string): number { - switch (input.toLowerCase()) { - case "easy": - return 1; - case "medium": - return 2; - case "hard": - return 3; - default: - return Number.MAX_SAFE_INTEGER; + private composeCategoryNodes(node: LeetCodeNode): LeetCodeNode[] { + const parent = node.id as Category; + const categoryNodes = Array.from(this.treeData[parent].keys()).map(subCategory => + new LeetCodeNode(Object.assign(list.IProblemDefault, { + id: node.name, + name: subCategory, + }), false) + ); + if (parent == "Difficulty") { + categoryNodes.sort((a: LeetCodeNode, b: LeetCodeNode): number => { + function getValue(input: string): number { + switch (input.toLowerCase()) { + case "easy": + return 1; + case "medium": + return 2; + case "hard": + return 3; + default: + return Number.MAX_SAFE_INTEGER; + } } - } - return getValue(a.name) - getValue(b.name); - }); - return difficultynodes; + return getValue(a.name) - getValue(b.name); + }); + } + return categoryNodes; } private parseIconPathFromProblemState(element: LeetCodeNode): string { From 64badc50a988e9437a01c07d7226d5ca1d9910b7 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Wed, 23 Jan 2019 08:42:12 +0800 Subject: [PATCH 05/14] Add `require-from-string` dependency --- package-lock.json | 5 +++++ package.json | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index b38c2e1e..44c4aa53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3031,6 +3031,11 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", diff --git a/package.json b/package.json index 367db0cd..d8c59e79 100644 --- a/package.json +++ b/package.json @@ -264,6 +264,7 @@ }, "dependencies": { "fs-extra": "^6.0.1", - "leetcode-cli": "2.6.1" + "leetcode-cli": "2.6.1", + "require-from-string": "^2.0.2" } } From d648f8a47f9ff95819f8a1e0c8301ad98bb19502 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Wed, 23 Jan 2019 09:17:25 +0800 Subject: [PATCH 06/14] Move getCompaniesAndTags from `list.ts` to `leetCodeExecutor.ts` Add unknown label to companies and tags --- src/commands/list.ts | 13 +++---------- src/leetCodeExecutor.ts | 23 ++++++++++++++++++----- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/commands/list.ts b/src/commands/list.ts index 89e40ff3..c0e03776 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -2,7 +2,6 @@ // Licensed under the MIT license. import * as vscode from "vscode"; -import * as path from "path"; import { leetCodeExecutor } from "../leetCodeExecutor"; import { leetCodeManager } from "../leetCodeManager"; import { ProblemState, UserStatus } from "../shared"; @@ -33,7 +32,7 @@ export async function listProblems(): Promise { const problems: IProblem[] = []; const lines: string[] = result.split("\n"); const reg: RegExp = /^(.)\s(.{1,2})\s(.)\s\[\s*(\d*)\s*\]\s*(.*)\s*(Easy|Medium|Hard)\s*\((\s*\d+\.\d+ %)\)/; - const { companies, tags } = await getCompaniesAndTags(); + const { companies, tags } = await leetCodeExecutor.getCompaniesAndTags(); for (const line of lines) { const match: RegExpMatchArray | null = line.match(reg); if (match && match.length === 8) { @@ -46,8 +45,8 @@ export async function listProblems(): Promise { name: match[5].trim(), difficulty: match[6].trim(), passRate: match[7].trim(), - companies: companies[id], - tags: tags[id] + companies: companies[id] || ["Unknown"], + tags: tags[id] || ["Unknown"] }); } } @@ -75,9 +74,3 @@ function parseProblemState(stateOutput: string): ProblemState { return ProblemState.Unknown; } } - -async function getCompaniesAndTags(): Promise<{ companies: { [key: string]: string[] }, tags: { [key: string]: string[] } }> { - const COMPONIES_TAGS_PATH = path.join(await leetCodeExecutor.getLeetCodeRootPath(), "lib", "plugins", "company.js"); - const { COMPONIES, TAGS } = require(COMPONIES_TAGS_PATH); - return { companies: COMPONIES, tags: TAGS }; -} diff --git a/src/leetCodeExecutor.ts b/src/leetCodeExecutor.ts index d48b848d..8a1a14f3 100644 --- a/src/leetCodeExecutor.ts +++ b/src/leetCodeExecutor.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import * as cp from "child_process"; +import * as fs from "fs"; import * as path from "path"; import * as vscode from "vscode"; import { Endpoint } from "./shared"; @@ -18,18 +19,18 @@ class LeetCodeExecutor { this.leetCodeRootPathInWsl = ""; } - public async getLeetCodeRootPath(): Promise { + public async getLeetCodeRootPath(): Promise { // not wrapped by "" if (wsl.useWsl()) { if (!this.leetCodeRootPathInWsl) { this.leetCodeRootPathInWsl = `${await wsl.toWslPath(this.leetCodeRootPath)}`; } - return `"${this.leetCodeRootPathInWsl}"`; + return `${this.leetCodeRootPathInWsl}`; } - return `"${this.leetCodeRootPath}"`; + return `${this.leetCodeRootPath}`; } - public async getLeetCodeBinaryPath(): Promise { - return path.join(await this.getLeetCodeRootPath(), "bin", "leetcode"); + public async getLeetCodeBinaryPath(): Promise { // wrapped by "" + return `"${path.join(await this.getLeetCodeRootPath(), "bin", "leetcode")}"`; } public async meetRequirements(): Promise { @@ -109,6 +110,18 @@ class LeetCodeExecutor { } } + public async getCompaniesAndTags(): Promise<{ companies: { [key: string]: string[] }, tags: { [key: string]: string[] } }> { + // preprocess the plugin source + const componiesTagsPath = path.join(await leetCodeExecutor.getLeetCodeRootPath(), "lib", "plugins", "company.js"); + let componiesTagsSrc = await fs.readFileSync(componiesTagsPath, "utf8"); + componiesTagsSrc = componiesTagsSrc.replace("module.exports = plugin", "module.exports = { COMPONIES, TAGS }"); + // require plugin from modified string + const requireFromString = require("require-from-string"); + const { COMPONIES, TAGS } = requireFromString(componiesTagsSrc, componiesTagsPath); + return { companies: COMPONIES, tags: TAGS }; + } + + private async executeCommandEx(command: string, args: string[], options: cp.SpawnOptions = { shell: true }): Promise { if (wsl.useWsl()) { return await executeCommand("wsl", [command].concat(args), options); From 21723feb15e0459800f997b6d7d10caa65d3097c Mon Sep 17 00:00:00 2001 From: Vigilans Date: Wed, 23 Jan 2019 09:43:27 +0800 Subject: [PATCH 07/14] Minor fixes --- src/leetCodeExplorer.ts | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/leetCodeExplorer.ts b/src/leetCodeExplorer.ts index 6ff7b0d4..d6b57b36 100644 --- a/src/leetCodeExplorer.ts +++ b/src/leetCodeExplorer.ts @@ -70,11 +70,11 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider parent node name, element.name -> node name label: element.isProblem ? `[${element.id}] ${element.name}` : element.name, - id: `${idPrefix}.${element.id}`, + id: `${idPrefix}.${element.id}.${element.name}`, collapsibleState: element.isProblem ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed, - contextValue: element.isProblem ? "problem" : "difficulty", + contextValue: element.isProblem ? "problem" : element.id.toLowerCase(), iconPath: this.parseIconPathFromProblemState(element), }; } @@ -82,32 +82,30 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider { if (!leetCodeManager.getUser()) { return [ - new LeetCodeNode( - Object.assign(list.IProblemDefault, { - id: "notSignIn", - name: "Sign in to LeetCode", - }), - false, - ), + new LeetCodeNode(Object.assign({}, list.IProblemDefault, { + id: "notSignIn", + name: "Sign in to LeetCode", + }), false), ]; } if (!element) { // Root view return new Promise(async (resolve: (res: LeetCodeNode[]) => void): Promise => { await this.getProblemData(); - resolve([ - new LeetCodeNode(Object.assign(list.IProblemDefault, { + const nodes = [ + new LeetCodeNode(Object.assign({}, list.IProblemDefault, { id: "Root", name: "Difficulty", }), false), - new LeetCodeNode(Object.assign(list.IProblemDefault, { + new LeetCodeNode(Object.assign({}, list.IProblemDefault, { id: "Root", name: "Tag", }), false), - new LeetCodeNode(Object.assign(list.IProblemDefault, { + new LeetCodeNode(Object.assign({}, list.IProblemDefault, { id: "Root", name: "Company", - }), false) - ]); + }), false), + ] + resolve(nodes); }); } else { switch (element.name) { // First-level @@ -134,12 +132,13 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider - new LeetCodeNode(Object.assign(list.IProblemDefault, { - id: node.name, + new LeetCodeNode(Object.assign({}, list.IProblemDefault, { + id: parent, name: subCategory, }), false) ); From 39071bf898bdee6ede8dcfd8c660c747305869a4 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Wed, 23 Jan 2019 10:15:20 +0800 Subject: [PATCH 08/14] Capitalize label name and sort labels --- src/leetCodeExplorer.ts | 48 +++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/leetCodeExplorer.ts b/src/leetCodeExplorer.ts index d6b57b36..6f82af4d 100644 --- a/src/leetCodeExplorer.ts +++ b/src/leetCodeExplorer.ts @@ -133,7 +133,8 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider c[0].toUpperCase() + c.slice(1)).join(' '); const problems = this.treeData[parent].get(subCategory); if (problems) { problems.push(problem); @@ -166,22 +167,37 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider { - function getValue(input: string): number { - switch (input.toLowerCase()) { - case "easy": - return 1; - case "medium": - return 2; - case "hard": - return 3; - default: - return Number.MAX_SAFE_INTEGER; + // Sort lists + switch (parent) { + case "Difficulty": { + categoryNodes.sort((a: LeetCodeNode, b: LeetCodeNode): number => { + function getValue(input: LeetCodeNode): number { + switch (input.name.toLowerCase()) { + case "easy": + return 1; + case "medium": + return 2; + case "hard": + return 3; + default: + return Number.MAX_SAFE_INTEGER; + } } - } - return getValue(a.name) - getValue(b.name); - }); + return getValue(a) - getValue(b); + }); + } + case "Tag": + case "Company": { + categoryNodes.sort((a: LeetCodeNode, b: LeetCodeNode): number => { + if (a.name == "Unknown") { + return 1; + } else if (b.name == "Unknown") { + return -1; + } else { + return Number(a.name > b.name) - Number(a.name < b.name); + } + }); + } } return categoryNodes; } From 3cb8743d03765c95839f207bbfad8d37c3ea1277 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Wed, 23 Jan 2019 12:50:03 +0800 Subject: [PATCH 09/14] Add favorite tree view --- src/leetCodeExplorer.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/leetCodeExplorer.ts b/src/leetCodeExplorer.ts index 6f82af4d..8448dbcb 100644 --- a/src/leetCodeExplorer.ts +++ b/src/leetCodeExplorer.ts @@ -42,8 +42,9 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider, Tag: Map, - Company: Map - } + Company: Map, + Favorite: list.IProblem[] + }; private onDidChangeTreeDataEvent: vscode.EventEmitter = new vscode.EventEmitter(); // tslint:disable-next-line:member-ordering @@ -104,6 +105,10 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider new LeetCodeNode(p)); default: // Second and lower levels return element.isProblem ? [] : this.composeProblemNodes(element); } @@ -123,9 +130,11 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider Date: Wed, 23 Jan 2019 13:27:42 +0800 Subject: [PATCH 10/14] Fix lint errors --- src/commands/list.ts | 11 ++++++----- src/leetCodeExecutor.ts | 11 ++++++----- src/leetCodeExplorer.ts | 43 ++++++++++++++++++++--------------------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/commands/list.ts b/src/commands/list.ts index c0e03776..72c67e8e 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -7,6 +7,7 @@ import { leetCodeManager } from "../leetCodeManager"; import { ProblemState, UserStatus } from "../shared"; import { DialogType, promptForOpenOutputChannel } from "../utils/uiUtils"; +// tslint:disable-next-line:typedef export const IProblemDefault = { favorite: false, locked: false, @@ -16,8 +17,8 @@ export const IProblemDefault = { difficulty: "", passRate: "", companies: [] as string[], - tags: [] as string[] -} + tags: [] as string[], +}; export type IProblem = typeof IProblemDefault; @@ -36,17 +37,17 @@ export async function listProblems(): Promise { for (const line of lines) { const match: RegExpMatchArray | null = line.match(reg); if (match && match.length === 8) { - const id = match[4].trim(); + const id: string = match[4].trim(); problems.push({ + id, favorite: match[1].trim().length > 0, locked: match[2].trim().length > 0, state: parseProblemState(match[3]), - id: id, name: match[5].trim(), difficulty: match[6].trim(), passRate: match[7].trim(), companies: companies[id] || ["Unknown"], - tags: tags[id] || ["Unknown"] + tags: tags[id] || ["Unknown"], }); } } diff --git a/src/leetCodeExecutor.ts b/src/leetCodeExecutor.ts index 8a1a14f3..ef354eb6 100644 --- a/src/leetCodeExecutor.ts +++ b/src/leetCodeExecutor.ts @@ -112,16 +112,17 @@ class LeetCodeExecutor { public async getCompaniesAndTags(): Promise<{ companies: { [key: string]: string[] }, tags: { [key: string]: string[] } }> { // preprocess the plugin source - const componiesTagsPath = path.join(await leetCodeExecutor.getLeetCodeRootPath(), "lib", "plugins", "company.js"); - let componiesTagsSrc = await fs.readFileSync(componiesTagsPath, "utf8"); - componiesTagsSrc = componiesTagsSrc.replace("module.exports = plugin", "module.exports = { COMPONIES, TAGS }"); + const componiesTagsPath: string = path.join(await leetCodeExecutor.getLeetCodeRootPath(), "lib", "plugins", "company.js"); + const componiesTagsSrc: string = (await fs.readFileSync(componiesTagsPath, "utf8")).replace( + "module.exports = plugin", + "module.exports = { COMPONIES, TAGS }", + ); // require plugin from modified string - const requireFromString = require("require-from-string"); + const requireFromString: (src: string, path: string) => any = require("require-from-string"); const { COMPONIES, TAGS } = requireFromString(componiesTagsSrc, componiesTagsPath); return { companies: COMPONIES, tags: TAGS }; } - private async executeCommandEx(command: string, args: string[], options: cp.SpawnOptions = { shell: true }): Promise { if (wsl.useWsl()) { return await executeCommand("wsl", [command].concat(args), options); diff --git a/src/leetCodeExplorer.ts b/src/leetCodeExplorer.ts index 8448dbcb..a64f2814 100644 --- a/src/leetCodeExplorer.ts +++ b/src/leetCodeExplorer.ts @@ -43,7 +43,7 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider, Tag: Map, Company: Map, - Favorite: list.IProblem[] + Favorite: list.IProblem[], }; private onDidChangeTreeDataEvent: vscode.EventEmitter = new vscode.EventEmitter(); @@ -92,7 +92,7 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider void): Promise => { await this.getProblemData(); - const nodes = [ + resolve([ new LeetCodeNode(Object.assign({}, list.IProblemDefault, { id: "Root", name: "Difficulty", @@ -109,8 +109,7 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider new LeetCodeNode(p)); + return this.treeData.Favorite.map((p: list.IProblem) => new LeetCodeNode(p)); default: // Second and lower levels return element.isProblem ? [] : this.composeProblemNodes(element); } @@ -131,20 +130,20 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider = [ ["Difficulty", [problem.difficulty]], ["Tag", problem.tags], - ["Company", problem.companies] - ] as [Category, string[]][]; + ["Company", problem.companies], + ]; for (const [parent, children] of categories) { for (let subCategory of children) { // map 'first-second' to 'First Second' - subCategory = subCategory.split('-').map(c => c[0].toUpperCase() + c.slice(1)).join(' '); - const problems = this.treeData[parent].get(subCategory); + subCategory = subCategory.split("-").map((c: string) => c[0].toUpperCase() + c.slice(1)).join(" "); + const problems: list.IProblem[] | undefined = this.treeData[parent].get(subCategory); if (problems) { problems.push(problem); } else { @@ -154,7 +153,7 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider - new LeetCodeNode(Object.assign({}, list.IProblemDefault, { - id: parent, - name: subCategory, - }), false) - ); + const parent: Category = node.name as Category; + const categoryNodes: LeetCodeNode[] = + Array.from(this.treeData[parent].keys()).map((subCategory: string) => + new LeetCodeNode(Object.assign({}, list.IProblemDefault, { + id: parent, + name: subCategory, + }), false)); // Sort lists switch (parent) { case "Difficulty": { @@ -202,9 +201,9 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider { - if (a.name == "Unknown") { + if (a.name === "Unknown") { return 1; - } else if (b.name == "Unknown") { + } else if (b.name === "Unknown") { return -1; } else { return Number(a.name > b.name) - Number(a.name < b.name); From 7e000629de9c7b5235e9a860104d048ad8f23aa8 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Thu, 24 Jan 2019 15:17:49 +0800 Subject: [PATCH 11/14] Show AC/NotAC problems in treeview --- src/leetCodeExplorer.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/leetCodeExplorer.ts b/src/leetCodeExplorer.ts index a64f2814..e5e7492c 100644 --- a/src/leetCodeExplorer.ts +++ b/src/leetCodeExplorer.ts @@ -44,6 +44,8 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider, Company: Map, Favorite: list.IProblem[], + Accepted: list.IProblem[], + NotAccepted: list.IProblem[], }; private onDidChangeTreeDataEvent: vscode.EventEmitter = new vscode.EventEmitter(); @@ -109,6 +111,14 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider new LeetCodeNode(p)); + case "Accepted": + return this.treeData.Accepted.map((p: list.IProblem) => new LeetCodeNode(p)); + case "Not Accepted": + return this.treeData.NotAccepted.map((p: list.IProblem) => new LeetCodeNode(p)); default: // Second and lower levels return element.isProblem ? [] : this.composeProblemNodes(element); } @@ -131,6 +145,8 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider Date: Thu, 24 Jan 2019 16:15:04 +0800 Subject: [PATCH 12/14] Revert "Show AC/NotAC problems in treeview" This reverts commit 7e000629de9c7b5235e9a860104d048ad8f23aa8. --- src/leetCodeExplorer.ts | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/leetCodeExplorer.ts b/src/leetCodeExplorer.ts index e5e7492c..a64f2814 100644 --- a/src/leetCodeExplorer.ts +++ b/src/leetCodeExplorer.ts @@ -44,8 +44,6 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider, Company: Map, Favorite: list.IProblem[], - Accepted: list.IProblem[], - NotAccepted: list.IProblem[], }; private onDidChangeTreeDataEvent: vscode.EventEmitter = new vscode.EventEmitter(); @@ -111,14 +109,6 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider new LeetCodeNode(p)); - case "Accepted": - return this.treeData.Accepted.map((p: list.IProblem) => new LeetCodeNode(p)); - case "Not Accepted": - return this.treeData.NotAccepted.map((p: list.IProblem) => new LeetCodeNode(p)); default: // Second and lower levels return element.isProblem ? [] : this.composeProblemNodes(element); } @@ -145,8 +131,6 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider Date: Thu, 24 Jan 2019 16:52:56 +0800 Subject: [PATCH 13/14] Option to hide solved problems --- package.json | 6 ++++++ src/leetCodeExplorer.ts | 14 ++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d8c59e79..af80dd4b 100644 --- a/package.json +++ b/package.json @@ -193,6 +193,12 @@ { "title": "LeetCode", "properties": { + "leetcode.hideSolved": { + "type": "boolean", + "default": false, + "scope": "application", + "description": "Hide solved problems." + }, "leetcode.showLocked": { "type": "boolean", "default": false, diff --git a/src/leetCodeExplorer.ts b/src/leetCodeExplorer.ts index a64f2814..24da0a53 100644 --- a/src/leetCodeExplorer.ts +++ b/src/leetCodeExplorer.ts @@ -46,6 +46,8 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider = new vscode.EventEmitter(); // tslint:disable-next-line:member-ordering public readonly onDidChangeTreeData: vscode.Event = this.onDidChangeTreeDataEvent.event; @@ -133,6 +135,14 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider("hideSolved")) { + continue; + } // Add problems according to category const categories: Array<[Category, string[]]> = [ ["Difficulty", [problem.difficulty]], @@ -151,10 +161,6 @@ export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider Date: Sat, 2 Feb 2019 13:23:52 +0800 Subject: [PATCH 14/14] Refactor --- package-lock.json | 50 ++--- package.json | 1 + src/commands/list.ts | 19 +- src/commands/show.ts | 8 +- src/explorer/LeetCodeNode.ts | 51 +++++ src/explorer/LeetCodeTreeDataProvider.ts | 238 ++++++++++++++++++++++ src/extension.ts | 3 +- src/leetCodeExecutor.ts | 7 +- src/leetCodeExplorer.ts | 241 ----------------------- src/shared.ts | 31 +++ src/utils/workspaceUtils.ts | 4 + 11 files changed, 364 insertions(+), 289 deletions(-) create mode 100644 src/explorer/LeetCodeNode.ts create mode 100644 src/explorer/LeetCodeTreeDataProvider.ts delete mode 100644 src/leetCodeExplorer.ts diff --git a/package-lock.json b/package-lock.json index 44c4aa53..4b6924ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,12 @@ "integrity": "sha512-iUdyWWikcQnGvIZnYh5ZxnxeREykndA9+iGdo068NGNutibWknDjmmNMq/8cnS1eaTCcgqJsPsFppw3XJWNlUg==", "dev": true }, + "@types/require-from-string": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/require-from-string/-/require-from-string-1.2.0.tgz", + "integrity": "sha512-5vE9WoOOC9/DoD3Zj53UISpM+5tSvh8k0mL4fe2zFI6vO715/W4IQ3EdVUrWVMrFi1/NZhyMvm2iKsDFkEGddQ==", + "dev": true + }, "abab": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", @@ -33,12 +39,12 @@ }, "acorn": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "resolved": "http://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=" }, "acorn-globals": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", + "resolved": "http://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", "optional": true, "requires": { @@ -192,7 +198,7 @@ }, "async": { "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" }, "asynckit": { @@ -374,7 +380,7 @@ }, "cheerio": { "version": "0.20.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.20.0.tgz", + "resolved": "http://registry.npmjs.org/cheerio/-/cheerio-0.20.0.tgz", "integrity": "sha1-XHEPK6uVZTJyhCugHG6mGzVF7DU=", "requires": { "css-select": "~1.2.0", @@ -562,7 +568,7 @@ }, "css-select": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "requires": { "boolbase": "~1.0.0", @@ -683,7 +689,7 @@ "dependencies": { "domelementtype": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=" } } @@ -997,7 +1003,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -1092,7 +1098,7 @@ }, "get-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" }, "getpass": { @@ -1503,7 +1509,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -1628,7 +1634,7 @@ }, "htmlparser2": { "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "resolved": "http://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", "requires": { "domelementtype": "1", @@ -1640,7 +1646,7 @@ "dependencies": { "entities": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/entities/-/entities-1.0.0.tgz", "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" } } @@ -1904,7 +1910,7 @@ }, "jsdom": { "version": "7.2.2", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-7.2.2.tgz", + "resolved": "http://registry.npmjs.org/jsdom/-/jsdom-7.2.2.tgz", "integrity": "sha1-QLQCdwwr2iNGkJa+6Rq2deOx/G4=", "optional": true, "requires": { @@ -2456,7 +2462,7 @@ "dependencies": { "yargs": { "version": "3.32.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", "requires": { "camelcase": "^2.0.1", @@ -2472,7 +2478,7 @@ }, "ncp": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz", "integrity": "sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY=" }, "nice-try": { @@ -2676,7 +2682,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "requires": { "lcid": "^1.0.0" @@ -2934,7 +2940,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "requires": { "core-util-is": "~1.0.0", @@ -3300,7 +3306,7 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "supports-color": { @@ -3564,7 +3570,7 @@ "dependencies": { "async": { "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "resolved": "http://registry.npmjs.org/async/-/async-0.9.2.tgz", "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" } } @@ -3804,7 +3810,7 @@ }, "winston": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/winston/-/winston-2.1.1.tgz", + "resolved": "http://registry.npmjs.org/winston/-/winston-2.1.1.tgz", "integrity": "sha1-PJNJ0ZYgf9G9/51LxD73JRDjoS4=", "requires": { "async": "~1.0.0", @@ -3818,12 +3824,12 @@ "dependencies": { "async": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/async/-/async-1.0.0.tgz", "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" }, "colors": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "resolved": "http://registry.npmjs.org/colors/-/colors-1.0.3.tgz", "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" }, "pkginfo": { @@ -3840,7 +3846,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "requires": { "string-width": "^1.0.1", diff --git a/package.json b/package.json index af80dd4b..307c8dbd 100644 --- a/package.json +++ b/package.json @@ -264,6 +264,7 @@ "@types/fs-extra": "5.0.0", "@types/mocha": "^2.2.42", "@types/node": "^7.0.43", + "@types/require-from-string": "^1.2.0", "tslint": "^5.9.1", "typescript": "^2.6.1", "vscode": "^1.1.22" diff --git a/src/commands/list.ts b/src/commands/list.ts index 72c67e8e..5cb44137 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -4,24 +4,9 @@ import * as vscode from "vscode"; import { leetCodeExecutor } from "../leetCodeExecutor"; import { leetCodeManager } from "../leetCodeManager"; -import { ProblemState, UserStatus } from "../shared"; +import { IProblem, ProblemState, UserStatus } from "../shared"; import { DialogType, promptForOpenOutputChannel } from "../utils/uiUtils"; -// tslint:disable-next-line:typedef -export const IProblemDefault = { - favorite: false, - locked: false, - state: ProblemState.Unknown, - id: "", - name: "", - difficulty: "", - passRate: "", - companies: [] as string[], - tags: [] as string[], -}; - -export type IProblem = typeof IProblemDefault; - export async function listProblems(): Promise { try { if (leetCodeManager.getStatus() === UserStatus.SignedOut) { @@ -40,7 +25,7 @@ export async function listProblems(): Promise { const id: string = match[4].trim(); problems.push({ id, - favorite: match[1].trim().length > 0, + isFavorite: match[1].trim().length > 0, locked: match[2].trim().length > 0, state: parseProblemState(match[3]), name: match[5].trim(), diff --git a/src/commands/show.ts b/src/commands/show.ts index 07cae45d..7d17a9dc 100644 --- a/src/commands/show.ts +++ b/src/commands/show.ts @@ -3,10 +3,10 @@ import * as fse from "fs-extra"; import * as vscode from "vscode"; +import { LeetCodeNode } from "../explorer/LeetCodeNode"; import { leetCodeExecutor } from "../leetCodeExecutor"; -import { LeetCodeNode } from "../leetCodeExplorer"; import { leetCodeManager } from "../leetCodeManager"; -import { IQuickItemEx, languages, ProblemState } from "../shared"; +import { IProblem, IQuickItemEx, languages, ProblemState } from "../shared"; import { DialogOptions, DialogType, promptForOpenOutputChannel, promptForSignIn } from "../utils/uiUtils"; import { selectWorkspaceFolder } from "../utils/workspaceUtils"; import * as wsl from "../utils/wslUtils"; @@ -80,9 +80,9 @@ async function showProblemInternal(id: string): Promise { } } -async function parseProblemsToPicks(p: Promise): Promise>> { +async function parseProblemsToPicks(p: Promise): Promise>> { return new Promise(async (resolve: (res: Array>) => void): Promise => { - const picks: Array> = (await p).map((problem: list.IProblem) => Object.assign({}, { + const picks: Array> = (await p).map((problem: IProblem) => Object.assign({}, { label: `${parseProblemDecorator(problem.state, problem.locked)}${problem.id}.${problem.name}`, description: "", detail: `AC rate: ${problem.passRate}, Difficulty: ${problem.difficulty}`, diff --git a/src/explorer/LeetCodeNode.ts b/src/explorer/LeetCodeNode.ts new file mode 100644 index 00000000..ad5211cb --- /dev/null +++ b/src/explorer/LeetCodeNode.ts @@ -0,0 +1,51 @@ +// Copyright (c) jdneo. All rights reserved. +// Licensed under the MIT license. + +import { IProblem, ProblemState } from "../shared"; + +export class LeetCodeNode { + constructor(private data: IProblem, private parentNodeName: string, private isProblemNode: boolean = true) { } + + 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; + } + + public get passRate(): string { + return this.data.passRate; + } + + public get difficulty(): string { + return this.data.difficulty; + } + + public get tags(): string[] { + return this.data.tags; + } + + public get companies(): string[] { + return this.data.companies; + } + + public get isFavorite(): boolean { + return this.data.isFavorite; + } + + public get isProblem(): boolean { + return this.isProblemNode; + } + + public get parentName(): string { + return this.parentNodeName; + } +} diff --git a/src/explorer/LeetCodeTreeDataProvider.ts b/src/explorer/LeetCodeTreeDataProvider.ts new file mode 100644 index 00000000..83a97534 --- /dev/null +++ b/src/explorer/LeetCodeTreeDataProvider.ts @@ -0,0 +1,238 @@ +// Copyright (c) jdneo. All rights reserved. +// Licensed under the MIT license. + +import * as path from "path"; +import * as vscode from "vscode"; +import * as list from "../commands/list"; +import { leetCodeChannel } from "../leetCodeChannel"; +import { leetCodeManager } from "../leetCodeManager"; +import { Category, defaultProblem, IProblem, ProblemState } from "../shared"; +import { getWorkspaceConfiguration } from "../utils/workspaceUtils"; +import { LeetCodeNode } from "./LeetCodeNode"; + +export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider { + + private treeData: { + Difficulty: Map, + Tag: Map, + Company: Map, + Favorite: IProblem[], + }; + + private onDidChangeTreeDataEvent: vscode.EventEmitter = new vscode.EventEmitter(); + // tslint:disable-next-line:member-ordering + public readonly onDidChangeTreeData: vscode.Event = this.onDidChangeTreeDataEvent.event; + + constructor(private context: vscode.ExtensionContext) { } + + public async refresh(): Promise { + await this.getProblemData(); + this.onDidChangeTreeDataEvent.fire(); + } + + public getTreeItem(element: LeetCodeNode): vscode.TreeItem | Thenable { + if (element.id === "notSignIn") { + return { + label: element.name, + id: element.id, + collapsibleState: vscode.TreeItemCollapsibleState.None, + command: { + command: "leetcode.signin", + title: "Sign in to LeetCode", + }, + }; + } + + const idPrefix: number = Date.now(); + return { + label: element.isProblem ? `[${element.id}] ${element.name}` : element.name, + 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 { + if (!leetCodeManager.getUser()) { + return [ + new LeetCodeNode(Object.assign({}, defaultProblem, { + id: "notSignIn", + name: "Sign in to LeetCode", + }), "ROOT", false), + ]; + } + if (!element) { // Root view + return new Promise(async (resolve: (res: LeetCodeNode[]) => void): Promise => { + await this.getProblemData(); + resolve([ + 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), + ]); + }); + } else { + switch (element.name) { // First-level + case Category.Favorite: + const nodes: IProblem[] = this.treeData[Category.Favorite]; + return nodes.map((p: IProblem) => new LeetCodeNode(p, Category.Favorite)); + case Category.Difficulty: + case Category.Tag: + case Category.Company: + return this.composeSubCategoryNodes(element); + default: // Second and lower levels + return element.isProblem ? [] : this.composeProblemNodes(element); + } + } + } + + private async getProblemData(): Promise { + // clear cache + this.treeData = { + Difficulty: new Map(), + Tag: new Map(), + Company: new Map(), + Favorite: [], + }; + for (const problem of await list.listProblems()) { + // Add favorite problem, no matter whether it is solved. + if (problem.isFavorite) { + this.treeData[Category.Favorite].push(problem); + } + // Hide solved problem in other category. + if (problem.state === ProblemState.AC && getWorkspaceConfiguration().get("hideSolved")) { + continue; + } + + this.addProblemToTreeData(problem); + } + } + + private composeProblemNodes(node: LeetCodeNode): LeetCodeNode[] { + const map: Map | undefined = this.treeData[node.parentName]; + if (!map) { + leetCodeChannel.appendLine(`Category: ${node.parentName} is not available.`); + return []; + } + const problems: IProblem[] = map.get(node.name) || []; + const problemNodes: LeetCodeNode[] = []; + for (const problem of problems) { + problemNodes.push(new LeetCodeNode(problem, node.name)); + } + return problemNodes; + } + + private composeSubCategoryNodes(node: LeetCodeNode): LeetCodeNode[] { + const category: Category = node.name as Category; + if (category === Category.Favorite) { + leetCodeChannel.appendLine("No sub-level for Favorite nodes"); + return []; + } + const map: Map | undefined = this.treeData[category]; + if (!map) { + leetCodeChannel.appendLine(`Category: ${category} is not available.`); + return []; + } + return this.getSubCategoryNodes(map, category); + } + + private parseIconPathFromProblemState(element: LeetCodeNode): string { + if (!element.isProblem) { + return ""; + } + switch (element.state) { + case ProblemState.AC: + return this.context.asAbsolutePath(path.join("resources", "check.png")); + case ProblemState.NotAC: + return this.context.asAbsolutePath(path.join("resources", "x.png")); + case ProblemState.Unknown: + if (element.locked) { + return this.context.asAbsolutePath(path.join("resources", "lock.png")); + } + return this.context.asAbsolutePath(path.join("resources", "blank.png")); + default: + return ""; + } + } + + private addProblemToTreeData(problem: IProblem): void { + this.putProblemToMap(this.treeData.Difficulty, problem.difficulty, problem); + for (const tag of problem.tags) { + this.putProblemToMap(this.treeData.Tag, this.beautifyCategoryName(tag), problem); + } + for (const company of problem.companies) { + this.putProblemToMap(this.treeData.Company, this.beautifyCategoryName(company), problem); + } + } + + private putProblemToMap(map: Map, key: string, problem: IProblem): void { + const problems: IProblem[] | undefined = map.get(key); + if (problems) { + problems.push(problem); + } else { + map.set(key, [problem]); + } + } + + private beautifyCategoryName(name: string): string { + return name.split("-").map((c: string) => c[0].toUpperCase() + c.slice(1)).join(" "); + } + + private getSubCategoryNodes(map: Map, category: Category): LeetCodeNode[] { + const subCategoryNodes: LeetCodeNode[] = Array.from(map.keys()).map((subCategory: string) => { + return new LeetCodeNode(Object.assign({}, defaultProblem, { + id: subCategory, + name: subCategory, + }), category.toString(), false); + }); + this.sortSubCategoryNodes(subCategoryNodes, category); + return subCategoryNodes; + } + + private sortSubCategoryNodes(subCategoryNodes: LeetCodeNode[], category: Category): void { + switch (category) { + case Category.Difficulty: + subCategoryNodes.sort((a: LeetCodeNode, b: LeetCodeNode): number => { + function getValue(input: LeetCodeNode): number { + switch (input.name.toLowerCase()) { + case "easy": + return 1; + case "medium": + return 2; + case "hard": + return 3; + default: + return Number.MAX_SAFE_INTEGER; + } + } + return getValue(a) - getValue(b); + }); + case Category.Tag: + case Category.Company: + subCategoryNodes.sort((a: LeetCodeNode, b: LeetCodeNode): number => { + if (a.name === "Unknown") { + return 1; + } else if (b.name === "Unknown") { + return -1; + } else { + return Number(a.name > b.name) - Number(a.name < b.name); + } + }); + default: + break; + } + } +} diff --git a/src/extension.ts b/src/extension.ts index 67b6c3ca..52b66536 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,9 +9,10 @@ import * as session from "./commands/session"; import * as show from "./commands/show"; import * as submit from "./commands/submit"; import * as test from "./commands/test"; +import { LeetCodeNode } from "./explorer/LeetCodeNode"; +import { LeetCodeTreeDataProvider } from "./explorer/LeetCodeTreeDataProvider"; import { leetCodeChannel } from "./leetCodeChannel"; import { leetCodeExecutor } from "./leetCodeExecutor"; -import { LeetCodeNode, LeetCodeTreeDataProvider } from "./leetCodeExplorer"; import { leetCodeManager } from "./leetCodeManager"; import { leetCodeResultProvider } from "./leetCodeResultProvider"; import { leetCodeStatusBarItem } from "./leetCodeStatusBarItem"; diff --git a/src/leetCodeExecutor.ts b/src/leetCodeExecutor.ts index ef354eb6..71d5188a 100644 --- a/src/leetCodeExecutor.ts +++ b/src/leetCodeExecutor.ts @@ -2,8 +2,9 @@ // Licensed under the MIT license. import * as cp from "child_process"; -import * as fs from "fs"; +import * as fse from "fs-extra"; import * as path from "path"; +import * as requireFromString from "require-from-string"; import * as vscode from "vscode"; import { Endpoint } from "./shared"; import { executeCommand, executeCommandWithProgress } from "./utils/cpUtils"; @@ -113,12 +114,10 @@ class LeetCodeExecutor { public async getCompaniesAndTags(): Promise<{ companies: { [key: string]: string[] }, tags: { [key: string]: string[] } }> { // preprocess the plugin source const componiesTagsPath: string = path.join(await leetCodeExecutor.getLeetCodeRootPath(), "lib", "plugins", "company.js"); - const componiesTagsSrc: string = (await fs.readFileSync(componiesTagsPath, "utf8")).replace( + const componiesTagsSrc: string = (await fse.readFile(componiesTagsPath, "utf8")).replace( "module.exports = plugin", "module.exports = { COMPONIES, TAGS }", ); - // require plugin from modified string - const requireFromString: (src: string, path: string) => any = require("require-from-string"); const { COMPONIES, TAGS } = requireFromString(componiesTagsSrc, componiesTagsPath); return { companies: COMPONIES, tags: TAGS }; } diff --git a/src/leetCodeExplorer.ts b/src/leetCodeExplorer.ts deleted file mode 100644 index 24da0a53..00000000 --- a/src/leetCodeExplorer.ts +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) jdneo. All rights reserved. -// Licensed under the MIT license. - -import * as path from "path"; -import * as vscode from "vscode"; -import * as list from "./commands/list"; -import { leetCodeManager } from "./leetCodeManager"; -import { ProblemState } from "./shared"; - -export type Category = "Difficulty" | "Tag" | "Company"; - -// tslint:disable:max-classes-per-file -export class LeetCodeNode { - constructor(private data: list.IProblem, private isProblemNode: boolean = true) { } - - 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; - } - - public get passRate(): string { - return this.data.passRate; - } - - public get isProblem(): boolean { - return this.isProblemNode; - } -} - -export class LeetCodeTreeDataProvider implements vscode.TreeDataProvider { - - private treeData: { - Difficulty: Map, - Tag: Map, - Company: Map, - Favorite: list.IProblem[], - }; - - private leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode"); - - private onDidChangeTreeDataEvent: vscode.EventEmitter = new vscode.EventEmitter(); - // tslint:disable-next-line:member-ordering - public readonly onDidChangeTreeData: vscode.Event = this.onDidChangeTreeDataEvent.event; - - constructor(private context: vscode.ExtensionContext) { } - - public async refresh(): Promise { - await this.getProblemData(); - this.onDidChangeTreeDataEvent.fire(); - } - - public getTreeItem(element: LeetCodeNode): vscode.TreeItem | Thenable { - if (element.id === "notSignIn") { - return { - label: element.name, - id: element.id, - collapsibleState: vscode.TreeItemCollapsibleState.None, - command: { - command: "leetcode.signin", - title: "Sign in to LeetCode", - }, - }; - } - - const idPrefix: number = Date.now(); - return { // element.id -> parent node name, element.name -> node name - label: element.isProblem ? `[${element.id}] ${element.name}` : element.name, - id: `${idPrefix}.${element.id}.${element.name}`, - 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 { - if (!leetCodeManager.getUser()) { - return [ - new LeetCodeNode(Object.assign({}, list.IProblemDefault, { - id: "notSignIn", - name: "Sign in to LeetCode", - }), false), - ]; - } - if (!element) { // Root view - return new Promise(async (resolve: (res: LeetCodeNode[]) => void): Promise => { - await this.getProblemData(); - resolve([ - new LeetCodeNode(Object.assign({}, list.IProblemDefault, { - id: "Root", - name: "Difficulty", - }), false), - new LeetCodeNode(Object.assign({}, list.IProblemDefault, { - id: "Root", - name: "Tag", - }), false), - new LeetCodeNode(Object.assign({}, list.IProblemDefault, { - id: "Root", - name: "Company", - }), false), - new LeetCodeNode(Object.assign({}, list.IProblemDefault, { - id: "Root", - name: "Favorite", - }), false), - ]); - }); - } else { - switch (element.name) { // First-level - case "Difficulty": - case "Tag": - case "Company": - return this.composeCategoryNodes(element); - case "Favorite": - return this.treeData.Favorite.map((p: list.IProblem) => new LeetCodeNode(p)); - default: // Second and lower levels - return element.isProblem ? [] : this.composeProblemNodes(element); - } - } - } - - private async getProblemData(): Promise { - this.treeData = { - Difficulty: new Map(), - Tag: new Map(), - Company: new Map(), - Favorite: [], - }; - for (const problem of await list.listProblems()) { - // Add favorite problem, no matter whether it is solved. - if (problem.favorite) { - this.treeData.Favorite.push(problem); - } - // Hide solved problem in category view is specified in option. - if (problem.state === ProblemState.AC && this.leetCodeConfig.get("hideSolved")) { - continue; - } - // Add problems according to category - const categories: Array<[Category, string[]]> = [ - ["Difficulty", [problem.difficulty]], - ["Tag", problem.tags], - ["Company", problem.companies], - ]; - for (const [parent, children] of categories) { - for (let subCategory of children) { - // map 'first-second' to 'First Second' - subCategory = subCategory.split("-").map((c: string) => c[0].toUpperCase() + c.slice(1)).join(" "); - const problems: list.IProblem[] | undefined = this.treeData[parent].get(subCategory); - if (problems) { - problems.push(problem); - } else { - this.treeData[parent].set(subCategory, [problem]); - } - } - } - } - } - - private composeProblemNodes(node: LeetCodeNode): LeetCodeNode[] { - // node.id stores the parent node name, node.name stores current node name. - const problems: list.IProblem[] | undefined = this.treeData[node.id].get(node.name); - if (!problems || problems.length === 0) { - return []; - } - const problemNodes: LeetCodeNode[] = []; - for (const problem of problems) { - problemNodes.push(new LeetCodeNode(problem)); - } - return problemNodes; - } - - private composeCategoryNodes(node: LeetCodeNode): LeetCodeNode[] { - const parent: Category = node.name as Category; - const categoryNodes: LeetCodeNode[] = - Array.from(this.treeData[parent].keys()).map((subCategory: string) => - new LeetCodeNode(Object.assign({}, list.IProblemDefault, { - id: parent, - name: subCategory, - }), false)); - // Sort lists - switch (parent) { - case "Difficulty": { - categoryNodes.sort((a: LeetCodeNode, b: LeetCodeNode): number => { - function getValue(input: LeetCodeNode): number { - switch (input.name.toLowerCase()) { - case "easy": - return 1; - case "medium": - return 2; - case "hard": - return 3; - default: - return Number.MAX_SAFE_INTEGER; - } - } - return getValue(a) - getValue(b); - }); - } - case "Tag": - case "Company": { - categoryNodes.sort((a: LeetCodeNode, b: LeetCodeNode): number => { - if (a.name === "Unknown") { - return 1; - } else if (b.name === "Unknown") { - return -1; - } else { - return Number(a.name > b.name) - Number(a.name < b.name); - } - }); - } - } - return categoryNodes; - } - - private parseIconPathFromProblemState(element: LeetCodeNode): string { - if (!element.isProblem) { - return ""; - } - switch (element.state) { - case ProblemState.AC: - return this.context.asAbsolutePath(path.join("resources", "check.png")); - case ProblemState.NotAC: - return this.context.asAbsolutePath(path.join("resources", "x.png")); - case ProblemState.Unknown: - if (element.locked) { - return this.context.asAbsolutePath(path.join("resources", "lock.png")); - } - return this.context.asAbsolutePath(path.join("resources", "blank.png")); - default: - return ""; - } - } -} diff --git a/src/shared.ts b/src/shared.ts index ec3653e8..5e58a351 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -39,3 +39,34 @@ export enum Endpoint { LeetCode = "leetcode", LeetCodeCN = "leetcode-cn", } + +export interface IProblem { + isFavorite: boolean; + locked: boolean; + state: ProblemState; + id: string; + name: string; + difficulty: string; + passRate: string; + companies: string[]; + tags: string[]; +} + +export const defaultProblem: IProblem = { + isFavorite: false, + locked: false, + state: ProblemState.Unknown, + id: "", + name: "", + difficulty: "", + passRate: "", + companies: [] as string[], + tags: [] as string[], +}; + +export enum Category { + Difficulty = "Difficulty", + Tag = "Tag", + Company = "Company", + Favorite = "Favorite", +} diff --git a/src/utils/workspaceUtils.ts b/src/utils/workspaceUtils.ts index 93662d09..3507c349 100644 --- a/src/utils/workspaceUtils.ts +++ b/src/utils/workspaceUtils.ts @@ -40,3 +40,7 @@ export async function getActivefilePath(uri?: vscode.Uri): Promise