diff --git a/package-lock.json b/package-lock.json index b38c2e1e..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", @@ -3031,6 +3037,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", @@ -3295,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": { @@ -3559,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=" } } @@ -3799,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", @@ -3813,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": { @@ -3835,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 367db0cd..307c8dbd 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, @@ -258,12 +264,14 @@ "@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" }, "dependencies": { "fs-extra": "^6.0.1", - "leetcode-cli": "2.6.1" + "leetcode-cli": "2.6.1", + "require-from-string": "^2.0.2" } } diff --git a/src/commands/list.ts b/src/commands/list.ts index 62295b4c..5cb44137 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -4,19 +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"; -export interface IProblem { - favorite: boolean; - locked: boolean; - state: ProblemState; - id: string; - name: string; - difficulty: string; - passRate: string; -} - export async function listProblems(): Promise { try { if (leetCodeManager.getStatus() === UserStatus.SignedOut) { @@ -28,17 +18,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 leetCodeExecutor.getCompaniesAndTags(); for (const line of lines) { const match: RegExpMatchArray | null = line.match(reg); if (match && match.length === 8) { + const id: string = match[4].trim(); problems.push({ - favorite: match[1].trim().length > 0, + id, + isFavorite: match[1].trim().length > 0, locked: match[2].trim().length > 0, state: parseProblemState(match[3]), - id: match[4].trim(), name: match[5].trim(), difficulty: match[6].trim(), passRate: match[7].trim(), + companies: companies[id] || ["Unknown"], + tags: tags[id] || ["Unknown"], }); } } 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 e6356a83..71d5188a 100644 --- a/src/leetCodeExecutor.ts +++ b/src/leetCodeExecutor.ts @@ -2,7 +2,9 @@ // Licensed under the MIT license. import * as cp from "child_process"; +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"; @@ -10,28 +12,31 @@ 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 { // not wrapped by "" 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 { // wrapped by "" + return `"${path.join(await this.getLeetCodeRootPath(), "bin", "leetcode")}"`; } 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 +47,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 { @@ -100,6 +111,17 @@ 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 fse.readFile(componiesTagsPath, "utf8")).replace( + "module.exports = plugin", + "module.exports = { COMPONIES, TAGS }", + ); + 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 deleted file mode 100644 index ecac75fd..00000000 --- a/src/leetCodeExplorer.ts +++ /dev/null @@ -1,182 +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"; - -// 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: Map = new Map(); - - 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.id}`, - collapsibleState: element.isProblem ? vscode.TreeItemCollapsibleState.None : vscode.TreeItemCollapsibleState.Collapsed, - contextValue: element.isProblem ? "problem" : "difficulty", - iconPath: this.parseIconPathFromProblemState(element), - }; - } - - public getChildren(element?: LeetCodeNode | undefined): vscode.ProviderResult { - if (!leetCodeManager.getUser()) { - return [ - new LeetCodeNode( - { - favorite: false, - locked: false, - state: ProblemState.Unknown, - id: "notSignIn", - name: "Sign in to LeetCode", - difficulty: "", - passRate: "", - }, - false, - ), - ]; - } - if (!element) { - return new Promise(async (resolve: (res: LeetCodeNode[]) => void): Promise => { - await this.getProblemData(); - resolve(this.composeDifficultyNodes()); - }); - } else { - return element.isProblem ? [] : this.composeProblemNodes(element.name); - } - } - - 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]); - } - } - } - - private composeProblemNodes(difficulty: string): LeetCodeNode[] { - const problems: list.IProblem[] | undefined = this.treeData.get(difficulty); - if (!problems || problems.length === 0) { - return []; - } - const problemNodes: LeetCodeNode[] = []; - for (const problem of problems) { - problemNodes.push(new LeetCodeNode(problem)); - } - return problemNodes; - } - - private composeDifficultyNodes(): LeetCodeNode[] { - const difficultynodes: LeetCodeNode[] = []; - for (const difficulty of this.treeData.keys()) { - difficultynodes.push( - new LeetCodeNode( - { - favorite: false, - locked: false, - state: ProblemState.Unknown, - id: difficulty, - name: difficulty, - difficulty: "", - passRate: "", - }, - false, - ), - ); - } - difficultynodes.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; - } - - 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