diff --git a/src/commands/submit.ts b/src/commands/submit.ts index 6ed100dc..2035ec11 100644 --- a/src/commands/submit.ts +++ b/src/commands/submit.ts @@ -6,7 +6,7 @@ import { leetCodeExecutor } from "../leetCodeExecutor"; import { leetCodeManager } from "../leetCodeManager"; import { DialogType, promptForOpenOutputChannel, promptForSignIn } from "../utils/uiUtils"; import { getActiveFilePath } from "../utils/workspaceUtils"; -import { leetCodeResultProvider } from "../webview/leetCodeResultProvider"; +import { leetCodeSubmissionProvider } from "../webview/leetCodeSubmissionProvider"; export async function submitSolution(uri?: vscode.Uri): Promise { if (!leetCodeManager.getUser()) { @@ -21,7 +21,7 @@ export async function submitSolution(uri?: vscode.Uri): Promise { try { const result: string = await leetCodeExecutor.submitSolution(filePath); - await leetCodeResultProvider.show(result); + await leetCodeSubmissionProvider.show(result); } catch (error) { await promptForOpenOutputChannel("Failed to submit the solution. Please open the output channel for details.", DialogType.error); } diff --git a/src/commands/test.ts b/src/commands/test.ts index bc388f69..558f7409 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -10,7 +10,7 @@ import { isWindows, usingCmd } from "../utils/osUtils"; import { DialogType, promptForOpenOutputChannel, showFileSelectDialog } from "../utils/uiUtils"; import { getActiveFilePath } from "../utils/workspaceUtils"; import * as wsl from "../utils/wslUtils"; -import { leetCodeResultProvider } from "../webview/leetCodeResultProvider"; +import { leetCodeSubmissionProvider } from "../webview/leetCodeSubmissionProvider"; export async function testSolution(uri?: vscode.Uri): Promise { try { @@ -81,7 +81,7 @@ export async function testSolution(uri?: vscode.Uri): Promise { if (!result) { return; } - await leetCodeResultProvider.show(result); + await leetCodeSubmissionProvider.show(result); } catch (error) { await promptForOpenOutputChannel("Failed to test the solution. Please open the output channel for details.", DialogType.error); } diff --git a/src/extension.ts b/src/extension.ts index 1be99807..6cf38966 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,8 +18,9 @@ import { leetCodeManager } from "./leetCodeManager"; import { leetCodeStatusBarController } from "./statusbar/leetCodeStatusBarController"; import { DialogType, promptForOpenOutputChannel } from "./utils/uiUtils"; import { leetCodePreviewProvider } from "./webview/leetCodePreviewProvider"; -import { leetCodeResultProvider } from "./webview/leetCodeResultProvider"; import { leetCodeSolutionProvider } from "./webview/leetCodeSolutionProvider"; +import { leetCodeSubmissionProvider } from "./webview/leetCodeSubmissionProvider"; +import { markdownEngine } from "./webview/markdownEngine"; export async function activate(context: vscode.ExtensionContext): Promise { try { @@ -33,17 +34,15 @@ export async function activate(context: vscode.ExtensionContext): Promise }); const leetCodeTreeDataProvider: LeetCodeTreeDataProvider = new LeetCodeTreeDataProvider(context); - leetCodePreviewProvider.initialize(context); - leetCodeResultProvider.initialize(context); - leetCodeSolutionProvider.initialize(context); context.subscriptions.push( leetCodeStatusBarController, leetCodeChannel, leetCodePreviewProvider, - leetCodeResultProvider, + leetCodeSubmissionProvider, leetCodeSolutionProvider, leetCodeExecutor, + markdownEngine, vscode.window.createTreeView("leetCodeExplorer", { treeDataProvider: leetCodeTreeDataProvider, showCollapseAll: true }), vscode.languages.registerCodeLensProvider({ scheme: "file" }, codeLensProvider), vscode.commands.registerCommand("leetcode.deleteCache", () => cache.deleteCache()), diff --git a/src/webview/LeetCodeWebview.ts b/src/webview/LeetCodeWebview.ts new file mode 100644 index 00000000..7b0e9b8d --- /dev/null +++ b/src/webview/LeetCodeWebview.ts @@ -0,0 +1,64 @@ +// Copyright (c) jdneo. All rights reserved. +// Licensed under the MIT license. + +import { ConfigurationChangeEvent, Disposable, ViewColumn, WebviewPanel, window, workspace } from "vscode"; +import { markdownEngine } from "./markdownEngine"; + +export abstract class LeetCodeWebview implements Disposable { + + protected panel: WebviewPanel | undefined; + private listeners: Disposable[] = []; + + public dispose(): void { + if (this.panel) { + this.panel.dispose(); + } + } + + protected showWebviewInternal(): void { + const { viewType, title, viewColumn, preserveFocus } = this.getWebviewOption(); + if (!this.panel) { + this.panel = window.createWebviewPanel(viewType, title, { viewColumn, preserveFocus }, { + enableScripts: true, + enableCommandUris: true, + enableFindWidget: true, + retainContextWhenHidden: true, + localResourceRoots: markdownEngine.localResourceRoots, + }); + this.panel.onDidDispose(this.onDidDisposeWebview, this, this.listeners); + this.panel.webview.onDidReceiveMessage(this.onDidReceiveMessage, this, this.listeners); + workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.listeners); + } else { + this.panel.title = title; + this.panel.reveal(viewColumn, preserveFocus); + } + this.panel.webview.html = this.getWebviewContent(); + } + + protected onDidDisposeWebview(): void { + this.panel = undefined; + for (const listener of this.listeners) { + listener.dispose(); + } + this.listeners = []; + } + + protected async onDidChangeConfiguration(event: ConfigurationChangeEvent): Promise { + if (this.panel && event.affectsConfiguration("markdown")) { + this.panel.webview.html = this.getWebviewContent(); + } + } + + protected async onDidReceiveMessage(_message: any): Promise { /* no special rule */ } + + protected abstract getWebviewOption(): ILeetCodeWebviewOption; + + protected abstract getWebviewContent(): string; +} + +export interface ILeetCodeWebviewOption { + viewType: string; + title: string; + viewColumn: ViewColumn; + preserveFocus?: boolean; +} diff --git a/src/webview/leetCodePreviewProvider.ts b/src/webview/leetCodePreviewProvider.ts index 5b57434a..9189d769 100644 --- a/src/webview/leetCodePreviewProvider.ts +++ b/src/webview/leetCodePreviewProvider.ts @@ -1,93 +1,39 @@ // Copyright (c) jdneo. All rights reserved. // Licensed under the MIT license. -import { commands, Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode"; +import { commands, ViewColumn } from "vscode"; import { leetCodeExecutor } from "../leetCodeExecutor"; import { IProblem } from "../shared"; +import { ILeetCodeWebviewOption, LeetCodeWebview } from "./LeetCodeWebview"; import { markdownEngine } from "./markdownEngine"; -class LeetCodePreviewProvider implements Disposable { +class LeetCodePreviewProvider extends LeetCodeWebview { - private context: ExtensionContext; private node: IProblem; - private panel: WebviewPanel | undefined; - - public initialize(context: ExtensionContext): void { - this.context = context; - } + private description: IDescription; public async show(node: IProblem): Promise { - // Fetch problem first before creating webview panel - const descString: string = await leetCodeExecutor.getDescription(node); - + this.description = this.parseDescription(await leetCodeExecutor.getDescription(node), node); this.node = node; - if (!this.panel) { - this.panel = window.createWebviewPanel("leetcode.preview", "Preview Problem", ViewColumn.One, { - enableScripts: true, - enableCommandUris: true, - enableFindWidget: true, - retainContextWhenHidden: true, - localResourceRoots: markdownEngine.localResourceRoots, - }); - - this.panel.webview.onDidReceiveMessage(async (message: IWebViewMessage) => { - switch (message.command) { - case "ShowProblem": { - await commands.executeCommand("leetcode.showProblem", this.node); - break; - } - } - }, this, this.context.subscriptions); - - this.panel.onDidDispose(() => { - this.panel = undefined; - }, null, this.context.subscriptions); - } - - const description: IDescription = this.parseDescription(descString, node); - this.panel.webview.html = this.getWebViewContent(description); - this.panel.title = `${node.name}: Preview`; - this.panel.reveal(ViewColumn.One); + this.showWebviewInternal(); } - public dispose(): void { - if (this.panel) { - this.panel.dispose(); - } - } - - private parseDescription(descString: string, problem: IProblem): IDescription { - const [ - /* title */, , - url, , - /* tags */, , - /* langs */, , - category, - difficulty, - likes, - dislikes, - /* accepted */, - /* submissions */, - /* testcase */, , - ...body - ] = descString.split("\n"); + protected getWebviewOption(): ILeetCodeWebviewOption { return { - title: problem.name, - url, - tags: problem.tags, - companies: problem.companies, - category: category.slice(2), - difficulty: difficulty.slice(2), - likes: likes.split(": ")[1].trim(), - dislikes: dislikes.split(": ")[1].trim(), - body: body.join("\n").replace(/
\s*([^]+?)\s*<\/pre>/g, "
$1
"), + viewType: "leetcode.preview", + title: `${this.node.name}: Preview`, + viewColumn: ViewColumn.One, }; } - private getWebViewContent(desc: IDescription): string { - const mdStyles: string = markdownEngine.getStyles(); - const buttonStyle: string = ` - - `; - const { title, url, category, difficulty, likes, dislikes, body } = desc; + `, + }; + const { title, url, category, difficulty, likes, dislikes, body } = this.description; const head: string = markdownEngine.render(`# [${title}](${url})`); const info: string = markdownEngine.render([ `| Category | Difficulty | Likes | Dislikes |`, @@ -117,7 +63,7 @@ class LeetCodePreviewProvider implements Disposable { `
`, `Tags`, markdownEngine.render( - desc.tags + this.description.tags .map((t: string) => `[\`${t}\`](https://leetcode.com/tag/${t})`) .join(" | "), ), @@ -127,7 +73,7 @@ class LeetCodePreviewProvider implements Disposable { `
`, `Companies`, markdownEngine.render( - desc.companies + this.description.companies .map((c: string) => `\`${c}\``) .join(" | "), ), @@ -137,8 +83,8 @@ class LeetCodePreviewProvider implements Disposable { - ${mdStyles} - ${buttonStyle} + ${markdownEngine.getStyles()} + ${button.style} ${head} @@ -146,19 +92,58 @@ class LeetCodePreviewProvider implements Disposable { ${tags} ${companies} ${body} - + ${button.element} `; } + protected onDidDisposeWebview(): void { + super.onDidDisposeWebview(); + delete this.node; + delete this.description; + } + + protected async onDidReceiveMessage(message: IWebViewMessage): Promise { + switch (message.command) { + case "ShowProblem": { + await commands.executeCommand("leetcode.showProblem", this.node); + break; + } + } + } + + private parseDescription(descString: string, problem: IProblem): IDescription { + const [ + /* title */, , + url, , + /* tags */, , + /* langs */, , + category, + difficulty, + likes, + dislikes, + /* accepted */, + /* submissions */, + /* testcase */, , + ...body + ] = descString.split("\n"); + return { + title: problem.name, + url, + tags: problem.tags, + companies: problem.companies, + category: category.slice(2), + difficulty: difficulty.slice(2), + likes: likes.split(": ")[1].trim(), + dislikes: dislikes.split(": ")[1].trim(), + body: body.join("\n").replace(/
\s*([^]+?)\s*<\/pre>/g, "
$1
"), + }; + } } interface IDescription { diff --git a/src/webview/leetCodeResultProvider.ts b/src/webview/leetCodeResultProvider.ts deleted file mode 100644 index 1b6d2a54..00000000 --- a/src/webview/leetCodeResultProvider.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) jdneo. All rights reserved. -// Licensed under the MIT license. - -import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode"; -import { markdownEngine } from "./markdownEngine"; - -class LeetCodeResultProvider 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.result", "LeetCode Results", ViewColumn.Two, { - retainContextWhenHidden: true, - enableFindWidget: true, - localResourceRoots: markdownEngine.localResourceRoots, - }); - - this.panel.onDidDispose(() => { - this.panel = undefined; - }, null, this.context.subscriptions); - } - - this.panel.webview.html = await this.provideHtmlContent(result); - this.panel.reveal(ViewColumn.Two); - } - - public dispose(): void { - if (this.panel) { - this.panel.dispose(); - } - } - - private async provideHtmlContent(result: string): Promise { - return ` - - - - - ${markdownEngine.getStyles()} - - -
${result.trim()}
- - `; - } -} - -export const leetCodeResultProvider: LeetCodeResultProvider = new LeetCodeResultProvider(); diff --git a/src/webview/leetCodeSolutionProvider.ts b/src/webview/leetCodeSolutionProvider.ts index 8a378208..b433d5ba 100644 --- a/src/webview/leetCodeSolutionProvider.ts +++ b/src/webview/leetCodeSolutionProvider.ts @@ -1,60 +1,31 @@ // Copyright (c) jdneo. All rights reserved. // Licensed under the MIT license. -import { Disposable, ExtensionContext, ViewColumn, WebviewPanel, window } from "vscode"; +import { ViewColumn } from "vscode"; import { IProblem } from "../shared"; +import { ILeetCodeWebviewOption, LeetCodeWebview } from "./LeetCodeWebview"; import { markdownEngine } from "./markdownEngine"; -class LeetCodeSolutionProvider implements Disposable { +class LeetCodeSolutionProvider extends LeetCodeWebview { - private context: ExtensionContext; - private panel: WebviewPanel | undefined; - - public initialize(context: ExtensionContext): void { - this.context = context; - } + private solution: Solution; public async show(solutionString: string, problem: IProblem): Promise { - if (!this.panel) { - this.panel = window.createWebviewPanel("leetCode.solution", "Top Voted Solution", ViewColumn.Active, { - retainContextWhenHidden: true, - enableFindWidget: true, - localResourceRoots: markdownEngine.localResourceRoots, - }); - - this.panel.onDidDispose(() => { - this.panel = undefined; - }, null, this.context.subscriptions); - } - - const solution: Solution = this.parseSolution(solutionString); - this.panel.title = `${problem.name}: Solution`; - this.panel.webview.html = this.getWebViewContent(solution); - this.panel.reveal(ViewColumn.Active); + this.solution = this.parseSolution(solutionString, problem); + this.showWebviewInternal(); } - public dispose(): void { - if (this.panel) { - this.panel.dispose(); - } - } - - 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; + protected getWebviewOption(): ILeetCodeWebviewOption { + return { + viewType: "leetcode.solution", + title: `${this.solution.problem}: Solution`, + viewColumn: ViewColumn.One, + }; } - private getWebViewContent(solution: Solution): string { + protected getWebviewContent(): string { const styles: string = markdownEngine.getStyles(); - const { title, url, lang, author, votes } = solution; + const { title, url, lang, author, votes } = this.solution; const head: string = markdownEngine.render(`# [${title}](${url})`); const auth: string = `[${author}](https://leetcode.com/${author}/)`; const info: string = markdownEngine.render([ @@ -62,8 +33,8 @@ class LeetCodeSolutionProvider implements Disposable { `| :------: | :------: | :------: |`, `| ${lang} | ${auth} | ${votes} |`, ].join("\n")); - const body: string = markdownEngine.render(solution.body, { - lang: solution.lang, + const body: string = markdownEngine.render(this.solution.body, { + lang: this.solution.lang, host: "https://discuss.leetcode.com/", }); return ` @@ -80,6 +51,25 @@ class LeetCodeSolutionProvider implements Disposable { `; } + + protected onDidDisposeWebview(): void { + super.onDidDisposeWebview(); + delete this.solution; + } + + private parseSolution(raw: string, problem: IProblem): 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; + solution.problem = problem.name; + return solution; + } } // tslint:disable-next-line:max-classes-per-file @@ -90,6 +80,7 @@ class Solution { public author: string = ""; public votes: string = ""; public body: string = ""; // Markdown supported + public problem: string = ""; } export const leetCodeSolutionProvider: LeetCodeSolutionProvider = new LeetCodeSolutionProvider(); diff --git a/src/webview/leetCodeSubmissionProvider.ts b/src/webview/leetCodeSubmissionProvider.ts new file mode 100644 index 00000000..d12e6f41 --- /dev/null +++ b/src/webview/leetCodeSubmissionProvider.ts @@ -0,0 +1,45 @@ +// Copyright (c) jdneo. All rights reserved. +// Licensed under the MIT license. + +import { ViewColumn } from "vscode"; +import { ILeetCodeWebviewOption, LeetCodeWebview } from "./LeetCodeWebview"; +import { markdownEngine } from "./markdownEngine"; + +class LeetCodeSubmissionProvider extends LeetCodeWebview { + + private result: string; + + public async show(result: string): Promise { + this.result = result; + this.showWebviewInternal(); + } + + protected getWebviewOption(): ILeetCodeWebviewOption { + return { + viewType: "leetcode.submission", + title: "Submission", + viewColumn: ViewColumn.Two, + }; + } + + protected getWebviewContent(): string { + return ` + + + + + ${markdownEngine.getStyles()} + + +
${this.result.trim()}
+ + `; + } + + protected onDidDisposeWebview(): void { + super.onDidDisposeWebview(); + delete this.result; + } +} + +export const leetCodeSubmissionProvider: LeetCodeSubmissionProvider = new LeetCodeSubmissionProvider(); diff --git a/tslint.json b/tslint.json index dec1de2e..34776be7 100644 --- a/tslint.json +++ b/tslint.json @@ -26,6 +26,10 @@ "property-declaration", "variable-declaration", "member-variable-declaration" + ], + "variable-name": [ + true, + "allow-leading-underscore" ] }, "rulesDirectory": []