From f23bfa2b9cd7daad4fd416ff22abd6813823af87 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Thu, 7 Mar 2019 21:32:33 +0800 Subject: [PATCH 01/10] Add `soluton` command and solution webview provider --- package.json | 14 +++++++ src/commands/show.ts | 67 +++++++++++++++++++++++---------- src/extension.ts | 4 ++ src/leetCodeExecutor.ts | 15 ++++++-- src/leetCodeSolutionProvider.ts | 54 ++++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 24 deletions(-) create mode 100644 src/leetCodeSolutionProvider.ts diff --git a/package.json b/package.json index 8a316d76..aecceb2a 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,11 @@ "dark": "resources/dark/search.svg" } }, + { + "command": "leetcode.showSolution", + "title": "Show Top Voted Solution", + "category": "LeetCode" + }, { "command": "leetcode.testSolution", "title": "Test in LeetCode", @@ -164,12 +169,21 @@ "command": "leetcode.showProblem", "when": "view == leetCodeExplorer && viewItem == problem", "group": "leetcode@1" + }, + { + "command": "leetcode.showSolution", + "when": "view == leetCodeExplorer && viewItem == problem", + "group": "leetcode@2" } ], "commandPalette": [ { "command": "leetcode.showProblem", "when": "never" + }, + { + "command": "leetcode.showSolution", + "when": "never" } ], "explorer/context": [ diff --git a/src/commands/show.ts b/src/commands/show.ts index d1ce1cf4..f0d2232b 100644 --- a/src/commands/show.ts +++ b/src/commands/show.ts @@ -8,6 +8,7 @@ import { LeetCodeNode } from "../explorer/LeetCodeNode"; import { leetCodeChannel } from "../leetCodeChannel"; import { leetCodeExecutor } from "../leetCodeExecutor"; import { leetCodeManager } from "../leetCodeManager"; +import { leetCodeSolutionProvider } from "../leetCodeSolutionProvider"; import { IProblem, IQuickItemEx, languages, ProblemState } from "../shared"; import { DialogOptions, DialogType, promptForOpenOutputChannel, promptForSignIn } from "../utils/uiUtils"; import { selectWorkspaceFolder } from "../utils/workspaceUtils"; @@ -39,18 +40,58 @@ export async function searchProblem(): Promise { await showProblemInternal(choice.value); } -async function showProblemInternal(node: IProblem): Promise { +export async function showSolution(node?: LeetCodeNode): Promise { + if (!node) { + return; + } + const language: string | undefined = await fetchProblemLanguage(); + if (!language) { + return; + } try { - const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode"); - let defaultLanguage: string | undefined = leetCodeConfig.get("defaultLanguage"); - if (defaultLanguage && languages.indexOf(defaultLanguage) < 0) { - defaultLanguage = undefined; + const solution: string = await leetCodeExecutor.showSolution(node, language); + await leetCodeSolutionProvider.show(solution); + } catch (error) { + await promptForOpenOutputChannel("Failed to fetch the top voted solution. Please open the output channel for details.", DialogType.error); + } +} + +// SUGGESTION: group config retriving into one file +async function fetchProblemLanguage(): Promise { + const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode"); + let defaultLanguage: string | undefined = leetCodeConfig.get("defaultLanguage"); + if (defaultLanguage && languages.indexOf(defaultLanguage) < 0) { + defaultLanguage = undefined; + } + const language: string | undefined = defaultLanguage || await vscode.window.showQuickPick(languages, { placeHolder: "Select the language you want to use" }); + // fire-and-forget default language query + (async (): Promise => { + if (!defaultLanguage && leetCodeConfig.get("showSetDefaultLanguageHint")) { + const choice: vscode.MessageItem | undefined = await vscode.window.showInformationMessage( + `Would you like to set '${language}' as your default language?`, + DialogOptions.yes, + DialogOptions.no, + DialogOptions.never, + ); + if (choice === DialogOptions.yes) { + leetCodeConfig.update("defaultLanguage", language, true /* UserSetting */); + } else if (choice === DialogOptions.never) { + leetCodeConfig.update("showSetDefaultLanguageHint", false, true /* UserSetting */); + } } - const language: string | undefined = defaultLanguage || await vscode.window.showQuickPick(languages, { placeHolder: "Select the language you want to use" }); + })(); + return language; +} + +async function showProblemInternal(node: IProblem): Promise { + try { + const language: string | undefined = await fetchProblemLanguage(); if (!language) { return; } + // SUGGESTION: group config retriving into one file + const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode"); let outDir: string = await selectWorkspaceFolder(); let relativePath: string = (leetCodeConfig.get("outputFolder") || "").trim(); const matchResult: RegExpMatchArray | null = relativePath.match(/\$\{(.*?)\}/); @@ -69,20 +110,6 @@ async function showProblemInternal(node: IProblem): Promise { const originFilePath: string = await leetCodeExecutor.showProblem(node, language, outDir); const filePath: string = wsl.useWsl() ? await wsl.toWinPath(originFilePath) : originFilePath; await vscode.window.showTextDocument(vscode.Uri.file(filePath), { preview: false }); - - if (!defaultLanguage && leetCodeConfig.get("showSetDefaultLanguageHint")) { - const choice: vscode.MessageItem | undefined = await vscode.window.showInformationMessage( - `Would you like to set '${language}' as your default language?`, - DialogOptions.yes, - DialogOptions.no, - DialogOptions.never, - ); - if (choice === DialogOptions.yes) { - leetCodeConfig.update("defaultLanguage", language, true /* UserSetting */); - } else if (choice === DialogOptions.never) { - leetCodeConfig.update("showSetDefaultLanguageHint", false, true /* UserSetting */); - } - } } catch (error) { await promptForOpenOutputChannel("Failed to show the problem. Please open the output channel for details.", DialogType.error); } diff --git a/src/extension.ts b/src/extension.ts index aaf2ee4f..b0ec8315 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -16,6 +16,7 @@ import { leetCodeChannel } from "./leetCodeChannel"; import { leetCodeExecutor } from "./leetCodeExecutor"; import { leetCodeManager } from "./leetCodeManager"; import { leetCodeResultProvider } from "./leetCodeResultProvider"; +import { leetCodeSolutionProvider } from "./leetCodeSolutionProvider"; import { leetCodeStatusBarItem } from "./leetCodeStatusBarItem"; export async function activate(context: vscode.ExtensionContext): Promise { @@ -30,11 +31,13 @@ export async function activate(context: vscode.ExtensionContext): Promise const leetCodeTreeDataProvider: LeetCodeTreeDataProvider = new LeetCodeTreeDataProvider(context); leetCodeResultProvider.initialize(context); + leetCodeSolutionProvider.initialize(context); context.subscriptions.push( leetCodeStatusBarItem, leetCodeChannel, leetCodeResultProvider, + leetCodeSolutionProvider, vscode.window.registerTreeDataProvider("leetCodeExplorer", leetCodeTreeDataProvider), vscode.languages.registerCodeLensProvider({ scheme: "file" }, codeLensProvider), vscode.commands.registerCommand("leetcode.deleteCache", () => cache.deleteCache()), @@ -45,6 +48,7 @@ export async function activate(context: vscode.ExtensionContext): Promise vscode.commands.registerCommand("leetcode.createSession", () => session.createSession()), vscode.commands.registerCommand("leetcode.showProblem", (node: LeetCodeNode) => show.showProblem(node)), vscode.commands.registerCommand("leetcode.searchProblem", () => show.searchProblem()), + vscode.commands.registerCommand("leetcode.showSolution", (node: LeetCodeNode) => show.showSolution(node)), vscode.commands.registerCommand("leetcode.refreshExplorer", () => leetCodeTreeDataProvider.refresh()), vscode.commands.registerCommand("leetcode.testSolution", (uri?: vscode.Uri) => test.testSolution(uri)), vscode.commands.registerCommand("leetcode.submitSolution", (uri?: vscode.Uri) => submit.submitSolution(uri)), diff --git a/src/leetCodeExecutor.ts b/src/leetCodeExecutor.ts index 373bda11..0396934d 100644 --- a/src/leetCodeExecutor.ts +++ b/src/leetCodeExecutor.ts @@ -48,10 +48,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"]); + for (const plugin of ["company", "solution.discuss"]) { + try { // Check plugin + await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-e", plugin]); + } catch (error) { // Download plugin and activate + await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-i", plugin]); + } } return true; } @@ -87,6 +89,11 @@ class LeetCodeExecutor { return filePath; } + public async showSolution(node: IProblem, language: string): Promise { + const solution: string = await this.executeCommandWithProgressEx("Fetching top voted solution from discussions...", "node", [await this.getLeetCodeBinaryPath(), "show", node.id, "--solution", "-l", language]); + return solution; + } + public async listSessions(): Promise { return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "session"]); } diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts new file mode 100644 index 00000000..6ea4ea74 --- /dev/null +++ b/src/leetCodeSolutionProvider.ts @@ -0,0 +1,54 @@ +// Copyright (c) jdneo. All rights reserved. +// Licensed under the MIT license. + +import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode"; + +class LeetCodeSolutionProvider implements Disposable { + + private context: ExtensionContext; + private panel: WebviewPanel | undefined; + + public initialize(context: ExtensionContext): void { + this.context = context; + } + + public async show(result: string): Promise { + if (!this.panel) { + this.panel = window.createWebviewPanel("leetCode", "LeetCode Top Voted Solution", ViewColumn.Active, { + retainContextWhenHidden: true, + enableFindWidget: true, + }); + + this.panel.onDidDispose(() => { + this.panel = undefined; + }, null, this.context.subscriptions); + } + + this.panel.webview.html = this.getWebViewContent(result); + this.panel.reveal(ViewColumn.Active); + } + + public dispose(): void { + if (this.panel) { + this.panel.dispose(); + } + } + + private getWebViewContent(result: string): string { + return ` + + + + + + LeetCode Top Voted Solution + + +
${result.trim()}
+ + + `; + } +} + +export const leetCodeSolutionProvider: LeetCodeSolutionProvider = new LeetCodeSolutionProvider(); From 2a83af250ea5103ce45e6882cce7c64e0ad4d6fc Mon Sep 17 00:00:00 2001 From: Vigilans Date: Thu, 7 Mar 2019 23:28:04 +0800 Subject: [PATCH 02/10] Add Solution class & solution raw string parsing --- src/leetCodeSolutionProvider.ts | 24 ++++++++++++++++++++---- src/shared.ts | 9 +++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts index 6ea4ea74..749c29a4 100644 --- a/src/leetCodeSolutionProvider.ts +++ b/src/leetCodeSolutionProvider.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode"; +import { Solution } from "./shared"; class LeetCodeSolutionProvider implements Disposable { @@ -12,9 +13,9 @@ class LeetCodeSolutionProvider implements Disposable { this.context = context; } - public async show(result: string): Promise { + public async show(solutionString: string): Promise { if (!this.panel) { - this.panel = window.createWebviewPanel("leetCode", "LeetCode Top Voted Solution", ViewColumn.Active, { + this.panel = window.createWebviewPanel("leetCode", "Top voted solution", ViewColumn.Active, { retainContextWhenHidden: true, enableFindWidget: true, }); @@ -24,7 +25,9 @@ class LeetCodeSolutionProvider implements Disposable { }, null, this.context.subscriptions); } - this.panel.webview.html = this.getWebViewContent(result); + const solution: Solution = this.parseSolution(solutionString); + this.panel.title = solution.title; + this.panel.webview.html = this.getWebViewContent(solution.body); this.panel.reveal(ViewColumn.Active); } @@ -34,6 +37,19 @@ class LeetCodeSolutionProvider implements Disposable { } } + private parseSolution(raw: string): Solution { + const solution: Solution = new Solution(); + // [^] matches everything including \n, yet can be replaced by . in ES2018's `m` flag + raw = raw.slice(1); // skip first empty line + [solution.title, raw] = raw.split(/\n\n([^]+)/); // parse title and skip one line + [solution.url, raw] = raw.split(/\n\n([^]+)/); // parse url and skip one line + [solution.lang, raw] = raw.match(/\* Lang:\s+(.+)\n([^]+)/)!.slice(1); + [solution.author, raw] = raw.match(/\* Author:\s+(.+)\n([^]+)/)!.slice(1); + [solution.votes, raw] = raw.match(/\* Votes:\s+(\d+)\n\n([^]+)/)!.slice(1); + solution.body = raw; + return solution; + } + private getWebViewContent(result: string): string { return ` @@ -44,7 +60,7 @@ class LeetCodeSolutionProvider implements Disposable { LeetCode Top Voted Solution -
${result.trim()}
+
${result}
`; diff --git a/src/shared.ts b/src/shared.ts index 942e5ba1..7fa81b4a 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -85,6 +85,15 @@ export const defaultProblem: IProblem = { tags: [] as string[], }; +export class Solution { + public title: string = ""; + public url: string = ""; + public lang: string = ""; + public author: string = ""; + public votes: string = ""; + public body: string = ""; // Markdown supported +} + export enum Category { Difficulty = "Difficulty", Tag = "Tag", From 221ff298f9774d8c0bd6a7dbb8a1fdb31b14369a Mon Sep 17 00:00:00 2001 From: Vigilans Date: Fri, 8 Mar 2019 01:03:24 +0800 Subject: [PATCH 03/10] Add markdown-it parser with syntax highlight --- package-lock.json | 70 ++++++++++++++++++++++++++++----- package.json | 8 +++- src/leetCodeSolutionProvider.ts | 58 +++++++++++++++++++-------- 3 files changed, 107 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1869c5ed..8031c3c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,18 @@ "@types/node": "*" } }, + "@types/highlight.js": { + "version": "9.12.3", + "resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.3.tgz", + "integrity": "sha512-pGF/zvYOACZ/gLGWdQH8zSwteQS1epp68yRcVLJMgUck/MjEn/FBYmPub9pXT8C1e4a8YZfHo1CKyV8q1vKUnQ==", + "dev": true + }, + "@types/linkify-it": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-2.0.4.tgz", + "integrity": "sha512-9o5piu3tP6DwqT+Cyf7S3BitsTc6Cl0pCPKUhIE5hzQbtueiBXdtBipTLLvaGfT11/8XHRmsagu4YfBesTaiCA==", + "dev": true + }, "@types/lodash": { "version": "4.14.121", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.121.tgz", @@ -28,6 +40,15 @@ "@types/lodash": "*" } }, + "@types/markdown-it": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-0.0.7.tgz", + "integrity": "sha512-WyL6pa76ollQFQNEaLVa41ZUUvDvPY+qAUmlsphnrpL6I9p1m868b26FyeoOmo7X3/Ta/S9WKXcEYXUSHnxoVQ==", + "dev": true, + "requires": { + "@types/linkify-it": "*" + } + }, "@types/mocha": { "version": "2.2.48", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.48.tgz", @@ -55,8 +76,7 @@ "acorn": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", - "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=", - "optional": true + "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=" }, "acorn-globals": { "version": "1.0.9", @@ -128,7 +148,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -528,8 +547,7 @@ "cssom": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", - "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==", - "optional": true + "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==" }, "cssstyle": { "version": "0.2.37", @@ -1270,6 +1288,11 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "highlight.js": { + "version": "9.15.6", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.6.tgz", + "integrity": "sha512-zozTAWM1D6sozHo8kqhfYgsac+B+q0PmsjXeyDrYIHHcBN0zTVT66+s2GW1GZv7DbyaROdLXKdabwS/WqPyIdQ==" + }, "htmlparser2": { "version": "3.8.3", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", @@ -1612,6 +1635,14 @@ "type-check": "~0.3.2" } }, + "linkify-it": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz", + "integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==", + "requires": { + "uc.micro": "^1.0.1" + } + }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -1653,6 +1684,23 @@ "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", "dev": true }, + "markdown-it": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", + "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", + "requires": { + "argparse": "^1.0.7", + "entities": "~1.1.1", + "linkify-it": "^2.0.0", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + } + }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" + }, "mem": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz", @@ -2129,8 +2177,7 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "optional": true + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "process-nextick-args": { "version": "2.0.0", @@ -2445,8 +2492,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { "version": "1.16.1", @@ -2735,7 +2781,6 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "optional": true, "requires": { "prelude-ls": "~1.1.2" } @@ -2746,6 +2791,11 @@ "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", "dev": true }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", diff --git a/package.json b/package.json index aecceb2a..d77751a6 100644 --- a/package.json +++ b/package.json @@ -287,7 +287,9 @@ }, "devDependencies": { "@types/fs-extra": "5.0.0", + "@types/highlight.js": "^9.12.3", "@types/lodash.kebabcase": "^4.1.5", + "@types/markdown-it": "0.0.7", "@types/mocha": "^2.2.42", "@types/node": "^7.0.43", "@types/require-from-string": "^1.2.0", @@ -297,8 +299,10 @@ }, "dependencies": { "fs-extra": "^6.0.1", - "vsc-leetcode-cli": "2.6.2", + "highlight.js": "^9.15.6", "lodash.kebabcase": "^4.1.1", - "require-from-string": "^2.0.2" + "markdown-it": "^8.4.2", + "require-from-string": "^2.0.2", + "vsc-leetcode-cli": "2.6.2" } } diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts index 749c29a4..e9cb13f4 100644 --- a/src/leetCodeSolutionProvider.ts +++ b/src/leetCodeSolutionProvider.ts @@ -1,6 +1,8 @@ // Copyright (c) jdneo. All rights reserved. // Licensed under the MIT license. +import * as hljs from "highlight.js"; +import * as MarkdownIt from "markdown-it"; import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode"; import { Solution } from "./shared"; @@ -8,9 +10,16 @@ class LeetCodeSolutionProvider implements Disposable { private context: ExtensionContext; private panel: WebviewPanel | undefined; + private markdown: MarkdownIt; + private solution: Solution; public initialize(context: ExtensionContext): void { this.context = context; + this.markdown = new MarkdownIt({ + linkify: true, + typographer: true, + highlight: this.codeHighlighter.bind(this), + }); } public async show(solutionString: string): Promise { @@ -25,9 +34,9 @@ class LeetCodeSolutionProvider implements Disposable { }, null, this.context.subscriptions); } - const solution: Solution = this.parseSolution(solutionString); - this.panel.title = solution.title; - this.panel.webview.html = this.getWebViewContent(solution.body); + this.solution = this.parseSolution(solutionString); + this.panel.title = this.solution.title; + this.panel.webview.html = this.getWebViewContent(this.solution.body); this.panel.reveal(ViewColumn.Active); } @@ -50,20 +59,35 @@ class LeetCodeSolutionProvider implements Disposable { return solution; } - private getWebViewContent(result: string): string { - return ` - - - - - - LeetCode Top Voted Solution - - -
${result}
- - - `; + private codeHighlighter(code: string, lang: string | undefined): string { + if (!lang) { + lang = this.solution.lang; + } + // tslint:disable-next-line:typedef + const hljst = hljs; + if (hljst.getLanguage(lang)) { + try { + return hljst.highlight(lang, code).value; + } catch (error) { /* do not highlight */ } + } + return ""; // use external default escaping + } + + private getWebViewContent(body: string): string { + return this.markdown.render(body); + // return ` + // + // + // + // + // + // LeetCode Top Voted Solution + // + // + //
${body}
+ // + // + // `; } } From 9c01549c77495cf1f7eb36797addd43609039374 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Fri, 8 Mar 2019 11:04:22 +0800 Subject: [PATCH 04/10] Add markdown style support for SolutionProvider --- src/leetCodeSolutionProvider.ts | 43 +++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts index e9cb13f4..981437fd 100644 --- a/src/leetCodeSolutionProvider.ts +++ b/src/leetCodeSolutionProvider.ts @@ -3,6 +3,8 @@ import * as hljs from "highlight.js"; import * as MarkdownIt from "markdown-it"; +import * as path from "path"; +import * as vscode from "vscode"; import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode"; import { Solution } from "./shared"; @@ -11,6 +13,7 @@ class LeetCodeSolutionProvider implements Disposable { private context: ExtensionContext; private panel: WebviewPanel | undefined; private markdown: MarkdownIt; + private markdownPath: string; // path of vscode built-in markdown extension private solution: Solution; public initialize(context: ExtensionContext): void { @@ -20,6 +23,7 @@ class LeetCodeSolutionProvider implements Disposable { typographer: true, highlight: this.codeHighlighter.bind(this), }); + this.markdownPath = path.join(process.env.VSCODE_CWD as string, "resources", "app", "extensions", "markdown-language-features"); } public async show(solutionString: string): Promise { @@ -27,6 +31,7 @@ class LeetCodeSolutionProvider implements Disposable { this.panel = window.createWebviewPanel("leetCode", "Top voted solution", ViewColumn.Active, { retainContextWhenHidden: true, enableFindWidget: true, + localResourceRoots: [vscode.Uri.file(path.join(this.markdownPath, "media"))], }); this.panel.onDidDispose(() => { @@ -36,7 +41,7 @@ class LeetCodeSolutionProvider implements Disposable { this.solution = this.parseSolution(solutionString); this.panel.title = this.solution.title; - this.panel.webview.html = this.getWebViewContent(this.solution.body); + this.panel.webview.html = this.getWebViewContent(this.solution); this.panel.reveal(ViewColumn.Active); } @@ -73,21 +78,27 @@ class LeetCodeSolutionProvider implements Disposable { return ""; // use external default escaping } - private getWebViewContent(body: string): string { - return this.markdown.render(body); - // return ` - // - // - // - // - // - // LeetCode Top Voted Solution - // - // - //
${body}
- // - // - // `; + private getMarkdownStyles(): vscode.Uri[] { + const stylePaths: string[] = require(path.join(this.markdownPath, "package.json"))["contributes"]["markdown.previewStyles"]; + return stylePaths.map((p: string) => vscode.Uri.file(path.join(this.markdownPath, p)).with({ scheme: "vscode-resource" })); + } + + private getWebViewContent(solution: Solution): string { + const styles: string = this.getMarkdownStyles() + .map((style: vscode.Uri) => ``) + .join("\n"); + const body: string = this.markdown.render(solution.body); + return ` + + + + ${styles} + + + ${body} + + + `; } } From 6478ebceb9849b1bd5376ea1630ca6caca258838 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Fri, 8 Mar 2019 12:07:12 +0800 Subject: [PATCH 05/10] Render cold_block with solution language in SolutionProvider --- src/leetCodeSolutionProvider.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts index 981437fd..397c0713 100644 --- a/src/leetCodeSolutionProvider.ts +++ b/src/leetCodeSolutionProvider.ts @@ -24,6 +24,17 @@ class LeetCodeSolutionProvider implements Disposable { highlight: this.codeHighlighter.bind(this), }); this.markdownPath = path.join(process.env.VSCODE_CWD as string, "resources", "app", "extensions", "markdown-language-features"); + + // Override code_block rule for highlighting in solution language + // tslint:disable-next-line:typedef + this.markdown.renderer.rules["code_block"] = (tokens, idx, options, _, self) => { + const highlight: string = options.highlight(tokens[idx].content, undefined); + return [ + `
`,
+                highlight || this.markdown.utils.escapeHtml(tokens[idx].content),
+                "
", + ].join("\n"); + }; } public async show(solutionString: string): Promise { @@ -68,11 +79,9 @@ class LeetCodeSolutionProvider implements Disposable { if (!lang) { lang = this.solution.lang; } - // tslint:disable-next-line:typedef - const hljst = hljs; - if (hljst.getLanguage(lang)) { + if (hljs.getLanguage(lang)) { try { - return hljst.highlight(lang, code).value; + return hljs.highlight(lang, code, true).value; } catch (error) { /* do not highlight */ } } return ""; // use external default escaping From 16bf8d62753820db94eb4a39a0a5fb62a44ea1cb Mon Sep 17 00:00:00 2001 From: Vigilans Date: Fri, 8 Mar 2019 12:47:11 +0800 Subject: [PATCH 06/10] Format modification: * fix tab-size to 4 * remove the redundant escape slash in solutionString --- src/commands/show.ts | 4 +++- src/leetCodeSolutionProvider.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/show.ts b/src/commands/show.ts index f0d2232b..4aed7f4b 100644 --- a/src/commands/show.ts +++ b/src/commands/show.ts @@ -49,7 +49,9 @@ export async function showSolution(node?: LeetCodeNode): Promise { return; } try { - const solution: string = await leetCodeExecutor.showSolution(node, language); + let solution: string = await leetCodeExecutor.showSolution(node, language); + // remove slash in espaced \'...\'(generated by leetcode's database) + solution = solution.replace(/\\'/g, "'"); await leetCodeSolutionProvider.show(solution); } catch (error) { await promptForOpenOutputChannel("Failed to fetch the top voted solution. Please open the output channel for details.", DialogType.error); diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts index 397c0713..d99d1927 100644 --- a/src/leetCodeSolutionProvider.ts +++ b/src/leetCodeSolutionProvider.ts @@ -103,7 +103,7 @@ class LeetCodeSolutionProvider implements Disposable { ${styles} - + ${body} From eaba0610c69f3997350befc8b7fca69f3f9632c5 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Fri, 8 Mar 2019 17:21:41 +0800 Subject: [PATCH 07/10] Enrich content in webview --- src/commands/show.ts | 2 +- src/leetCodeSolutionProvider.ts | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/commands/show.ts b/src/commands/show.ts index 4aed7f4b..cdd8700a 100644 --- a/src/commands/show.ts +++ b/src/commands/show.ts @@ -52,7 +52,7 @@ export async function showSolution(node?: LeetCodeNode): Promise { let solution: string = await leetCodeExecutor.showSolution(node, language); // remove slash in espaced \'...\'(generated by leetcode's database) solution = solution.replace(/\\'/g, "'"); - await leetCodeSolutionProvider.show(solution); + await leetCodeSolutionProvider.show(solution, node); } catch (error) { await promptForOpenOutputChannel("Failed to fetch the top voted solution. Please open the output channel for details.", DialogType.error); } diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts index d99d1927..bfb55ed7 100644 --- a/src/leetCodeSolutionProvider.ts +++ b/src/leetCodeSolutionProvider.ts @@ -6,7 +6,7 @@ import * as MarkdownIt from "markdown-it"; import * as path from "path"; import * as vscode from "vscode"; import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode"; -import { Solution } from "./shared"; +import { IProblem, Solution } from "./shared"; class LeetCodeSolutionProvider implements Disposable { @@ -37,7 +37,7 @@ class LeetCodeSolutionProvider implements Disposable { }; } - public async show(solutionString: string): Promise { + public async show(solutionString: string, problem: IProblem): Promise { if (!this.panel) { this.panel = window.createWebviewPanel("leetCode", "Top voted solution", ViewColumn.Active, { retainContextWhenHidden: true, @@ -51,7 +51,7 @@ class LeetCodeSolutionProvider implements Disposable { } this.solution = this.parseSolution(solutionString); - this.panel.title = this.solution.title; + this.panel.title = problem.name; this.panel.webview.html = this.getWebViewContent(this.solution); this.panel.reveal(ViewColumn.Active); } @@ -96,6 +96,14 @@ class LeetCodeSolutionProvider implements Disposable { const styles: string = this.getMarkdownStyles() .map((style: vscode.Uri) => ``) .join("\n"); + const { title, url, lang, author, votes } = solution; + const head: string = this.markdown.render(`# [${title}](${url})`); + const auth: string = `[${author}](https://leetcode.com/${author}/)`; + const info: string = this.markdown.render([ + `| Language | Author | Votes |`, + `| :------: | :------: | :------: |`, + `| ${lang} | ${auth} | ${votes} |`, + ].join("\n")); const body: string = this.markdown.render(solution.body); return ` @@ -104,6 +112,8 @@ class LeetCodeSolutionProvider implements Disposable { ${styles} + ${head} + ${info} ${body} From 7f39d6023d423590af9c27a6cc104a44a19f2e21 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Mon, 11 Mar 2019 22:49:38 +0800 Subject: [PATCH 08/10] Minor fixes --- src/commands/show.ts | 2 +- src/leetCodeExecutor.ts | 4 ++-- src/leetCodeSolutionProvider.ts | 12 +++++++++++- src/shared.ts | 9 --------- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/commands/show.ts b/src/commands/show.ts index cdd8700a..b535ea95 100644 --- a/src/commands/show.ts +++ b/src/commands/show.ts @@ -50,7 +50,7 @@ export async function showSolution(node?: LeetCodeNode): Promise { } try { let solution: string = await leetCodeExecutor.showSolution(node, language); - // remove slash in espaced \'...\'(generated by leetcode's database) + // remove backslash in espaced \'...\'(generated by leetcode's database) solution = solution.replace(/\\'/g, "'"); await leetCodeSolutionProvider.show(solution, node); } catch (error) { diff --git a/src/leetCodeExecutor.ts b/src/leetCodeExecutor.ts index 0396934d..71d227fb 100644 --- a/src/leetCodeExecutor.ts +++ b/src/leetCodeExecutor.ts @@ -89,8 +89,8 @@ class LeetCodeExecutor { return filePath; } - public async showSolution(node: IProblem, language: string): Promise { - const solution: string = await this.executeCommandWithProgressEx("Fetching top voted solution from discussions...", "node", [await this.getLeetCodeBinaryPath(), "show", node.id, "--solution", "-l", language]); + public async showSolution(problemNode: IProblem, language: string): Promise { + const solution: string = await this.executeCommandWithProgressEx("Fetching top voted solution from discussions...", "node", [await this.getLeetCodeBinaryPath(), "show", problemNode.id, "--solution", "-l", language]); return solution; } diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts index bfb55ed7..be990a09 100644 --- a/src/leetCodeSolutionProvider.ts +++ b/src/leetCodeSolutionProvider.ts @@ -6,7 +6,7 @@ import * as MarkdownIt from "markdown-it"; import * as path from "path"; import * as vscode from "vscode"; import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode"; -import { IProblem, Solution } from "./shared"; +import { IProblem } from "./shared"; class LeetCodeSolutionProvider implements Disposable { @@ -121,4 +121,14 @@ class LeetCodeSolutionProvider implements Disposable { } } +// tslint:disable-next-line:max-classes-per-file +class Solution { + public title: string = ""; + public url: string = ""; + public lang: string = ""; + public author: string = ""; + public votes: string = ""; + public body: string = ""; // Markdown supported +} + export const leetCodeSolutionProvider: LeetCodeSolutionProvider = new LeetCodeSolutionProvider(); diff --git a/src/shared.ts b/src/shared.ts index 7fa81b4a..942e5ba1 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -85,15 +85,6 @@ export const defaultProblem: IProblem = { tags: [] as string[], }; -export class Solution { - public title: string = ""; - public url: string = ""; - public lang: string = ""; - public author: string = ""; - public votes: string = ""; - public body: string = ""; // Markdown supported -} - export enum Category { Difficulty = "Difficulty", Tag = "Tag", From 3ca637cf08eb579d7f4f1430659d982f6820fd5e Mon Sep 17 00:00:00 2001 From: Vigilans Date: Tue, 12 Mar 2019 20:24:42 +0800 Subject: [PATCH 09/10] Minor fixes --- src/leetCodeExecutor.ts | 11 +++++------ src/leetCodeSolutionProvider.ts | 12 +++++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/leetCodeExecutor.ts b/src/leetCodeExecutor.ts index 71d227fb..b7b5f918 100644 --- a/src/leetCodeExecutor.ts +++ b/src/leetCodeExecutor.ts @@ -48,12 +48,11 @@ class LeetCodeExecutor { } return false; } - for (const plugin of ["company", "solution.discuss"]) { - try { // Check plugin - await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-e", plugin]); - } catch (error) { // Download plugin and activate - await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-i", plugin]); - } + for (const plugin of ["company", "solution.discuss", "leetcode.cn"]) { // Make sure plugin exists + await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-i", plugin]); + } + for (const plugin of ["company", "solution.discuss"]) { // activate plugin + await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-e", plugin]); } return true; } diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts index be990a09..a3fd3ab6 100644 --- a/src/leetCodeSolutionProvider.ts +++ b/src/leetCodeSolutionProvider.ts @@ -7,6 +7,7 @@ import * as path from "path"; import * as vscode from "vscode"; import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode"; import { IProblem } from "./shared"; +import { DialogType, promptForOpenOutputChannel } from "./utils/uiUtils"; class LeetCodeSolutionProvider implements Disposable { @@ -23,7 +24,7 @@ class LeetCodeSolutionProvider implements Disposable { typographer: true, highlight: this.codeHighlighter.bind(this), }); - this.markdownPath = path.join(process.env.VSCODE_CWD as string, "resources", "app", "extensions", "markdown-language-features"); + this.markdownPath = path.join(vscode.env.appRoot, "extensions", "markdown-language-features"); // Override code_block rule for highlighting in solution language // tslint:disable-next-line:typedef @@ -88,8 +89,13 @@ class LeetCodeSolutionProvider implements Disposable { } private getMarkdownStyles(): vscode.Uri[] { - const stylePaths: string[] = require(path.join(this.markdownPath, "package.json"))["contributes"]["markdown.previewStyles"]; - return stylePaths.map((p: string) => vscode.Uri.file(path.join(this.markdownPath, p)).with({ scheme: "vscode-resource" })); + try { + const stylePaths: string[] = require(path.join(this.markdownPath, "package.json"))["contributes"]["markdown.previewStyles"]; + return stylePaths.map((p: string) => vscode.Uri.file(path.join(this.markdownPath, p)).with({ scheme: "vscode-resource" })); + } catch (error) { + promptForOpenOutputChannel("Fail to load built-in markdown style file.", DialogType.error); + return []; + } } private getWebViewContent(solution: Solution): string { From c033f24710f440f576951ea8dd31ed815aca1c53 Mon Sep 17 00:00:00 2001 From: Vigilans Date: Wed, 13 Mar 2019 16:59:22 +0800 Subject: [PATCH 10/10] Minor fixes according to review * Revert plugin modification * Move markdown style error prompt from dialog to channel --- src/leetCodeExecutor.ts | 11 ++++++----- src/leetCodeSolutionProvider.ts | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/leetCodeExecutor.ts b/src/leetCodeExecutor.ts index 2f3e8b53..5d76fdd1 100644 --- a/src/leetCodeExecutor.ts +++ b/src/leetCodeExecutor.ts @@ -48,11 +48,12 @@ class LeetCodeExecutor { } return false; } - for (const plugin of ["company", "solution.discuss", "leetcode.cn"]) { // Make sure plugin exists - await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-i", plugin]); - } - for (const plugin of ["company", "solution.discuss"]) { // activate plugin - await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-e", plugin]); + for (const plugin of ["company", "solution.discuss"]) { + try { // Check plugin + await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-e", plugin]); + } catch (error) { // Download plugin and activate + await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "plugin", "-i", plugin]); + } } return true; } diff --git a/src/leetCodeSolutionProvider.ts b/src/leetCodeSolutionProvider.ts index a3fd3ab6..afbf88d6 100644 --- a/src/leetCodeSolutionProvider.ts +++ b/src/leetCodeSolutionProvider.ts @@ -6,8 +6,8 @@ import * as MarkdownIt from "markdown-it"; import * as path from "path"; import * as vscode from "vscode"; import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode"; +import { leetCodeChannel } from "./leetCodeChannel"; import { IProblem } from "./shared"; -import { DialogType, promptForOpenOutputChannel } from "./utils/uiUtils"; class LeetCodeSolutionProvider implements Disposable { @@ -93,7 +93,7 @@ class LeetCodeSolutionProvider implements Disposable { const stylePaths: string[] = require(path.join(this.markdownPath, "package.json"))["contributes"]["markdown.previewStyles"]; return stylePaths.map((p: string) => vscode.Uri.file(path.join(this.markdownPath, p)).with({ scheme: "vscode-resource" })); } catch (error) { - promptForOpenOutputChannel("Fail to load built-in markdown style file.", DialogType.error); + leetCodeChannel.appendLine("[Error] Fail to load built-in markdown style file."); return []; } }