diff --git a/src/commands/show.ts b/src/commands/show.ts index 3aebce8..9a3c141 100644 --- a/src/commands/show.ts +++ b/src/commands/show.ts @@ -11,278 +11,384 @@ import { leetCodeChannel } from "../leetCodeChannel"; import { leetCodeExecutor } from "../leetCodeExecutor"; import { leetCodeManager } from "../leetCodeManager"; import { IProblem, IQuickItemEx, languages, ProblemState } from "../shared"; -import { genFileExt, genFileName, getNodeIdFromFile } from "../utils/problemUtils"; +import { + genFileExt, + genFileName, + getNodeIdFromFile, +} from "../utils/problemUtils"; import * as settingUtils from "../utils/settingUtils"; import { IDescriptionConfiguration } from "../utils/settingUtils"; -import { DialogOptions, DialogType, openSettingsEditor, promptForOpenOutputChannel, promptForSignIn, promptHintMessage } from "../utils/uiUtils"; -import { getActiveFilePath, selectWorkspaceFolder } from "../utils/workspaceUtils"; +import { + DialogOptions, + DialogType, + openSettingsEditor, + promptForOpenOutputChannel, + promptForSignIn, + promptHintMessage, +} from "../utils/uiUtils"; +import { + getActiveFilePath, + selectWorkspaceFolder, +} from "../utils/workspaceUtils"; import * as wsl from "../utils/wslUtils"; import { leetCodePreviewProvider } from "../webview/leetCodePreviewProvider"; import { leetCodeSolutionProvider } from "../webview/leetCodeSolutionProvider"; import * as list from "./list"; -export async function previewProblem(input: IProblem | vscode.Uri, isSideMode: boolean = false): Promise { - let node: IProblem; - if (input instanceof vscode.Uri) { - const activeFilePath: string = input.fsPath; - const id: string = await getNodeIdFromFile(activeFilePath); - if (!id) { - vscode.window.showErrorMessage(`Failed to resolve the problem id from file: ${activeFilePath}.`); - return; - } - const cachedNode: IProblem | undefined = explorerNodeManager.getNodeById(id); - if (!cachedNode) { - vscode.window.showErrorMessage(`Failed to resolve the problem with id: ${id}.`); - return; - } - node = cachedNode; - // Move the preview page aside if it's triggered from Code Lens - isSideMode = true; - } else { - node = input; +export async function previewProblem( + input: IProblem | vscode.Uri, + isSideMode: boolean = false +): Promise { + let node: IProblem; + if (input instanceof vscode.Uri) { + const activeFilePath: string = input.fsPath; + const id: string = await getNodeIdFromFile(activeFilePath); + if (!id) { + vscode.window.showErrorMessage( + `Failed to resolve the problem id from file: ${activeFilePath}.` + ); + return; } - const needTranslation: boolean = settingUtils.shouldUseEndpointTranslation(); - const descString: string = await leetCodeExecutor.getDescription(node.id, needTranslation); - leetCodePreviewProvider.show(descString, node, isSideMode); + const cachedNode: IProblem | undefined = + explorerNodeManager.getNodeById(id); + if (!cachedNode) { + vscode.window.showErrorMessage( + `Failed to resolve the problem with id: ${id}.` + ); + return; + } + node = cachedNode; + // Move the preview page aside if it's triggered from Code Lens + isSideMode = true; + } else { + node = input; + } + const needTranslation: boolean = settingUtils.shouldUseEndpointTranslation(); + const descString: string = await leetCodeExecutor.getDescription( + node.id, + needTranslation + ); + leetCodePreviewProvider.show(descString, node, isSideMode); } export async function pickOne(): Promise { - const problems: IProblem[] = await list.listProblems(); - const randomProblem: IProblem = problems[Math.floor(Math.random() * problems.length)]; - await showProblemInternal(randomProblem); + const problems: IProblem[] = await list.listProblems(); + const randomProblem: IProblem = + problems[Math.floor(Math.random() * problems.length)]; + await showProblemInternal(randomProblem); } export async function showProblem(node?: LeetCodeNode): Promise { - if (!node) { - return; - } - await showProblemInternal(node); + if (!node) { + return; + } + await showProblemInternal(node); } export async function searchProblem(): Promise { - if (!leetCodeManager.getUser()) { - promptForSignIn(); - return; - } - const choice: IQuickItemEx | undefined = await vscode.window.showQuickPick( - parseProblemsToPicks(list.listProblems()), - { - matchOnDetail: true, - placeHolder: "Select one problem", - }, + if (!leetCodeManager.getUser()) { + promptForSignIn(); + return; + } + const choice: IQuickItemEx | undefined = + await vscode.window.showQuickPick( + parseProblemsToPicks(list.listProblems()), + { + matchOnDetail: true, + placeHolder: "Select one problem", + } ); - if (!choice) { - return; - } - await showProblemInternal(choice.value); + if (!choice) { + return; + } + await showProblemInternal(choice.value); } -export async function showSolution(input: LeetCodeNode | vscode.Uri): Promise { - let problemInput: string | undefined; - if (input instanceof LeetCodeNode) { // Triggerred from explorer - problemInput = input.id; - } else if (input instanceof vscode.Uri) { // Triggerred from Code Lens/context menu - problemInput = `"${input.fsPath}"`; - } else if (!input) { // Triggerred from command - problemInput = await getActiveFilePath(); - } +export async function showSolution( + input: LeetCodeNode | vscode.Uri +): Promise { + let problemInput: string | undefined; + if (input instanceof LeetCodeNode) { + // Triggerred from explorer + problemInput = input.id; + } else if (input instanceof vscode.Uri) { + // Triggerred from Code Lens/context menu + problemInput = `"${input.fsPath}"`; + } else if (!input) { + // Triggerred from command + problemInput = await getActiveFilePath(); + } - if (!problemInput) { - vscode.window.showErrorMessage("Invalid input to fetch the solution data."); - return; - } + if (!problemInput) { + vscode.window.showErrorMessage("Invalid input to fetch the solution data."); + return; + } - const language: string | undefined = await fetchProblemLanguage(); - if (!language) { - return; - } - try { - const needTranslation: boolean = settingUtils.shouldUseEndpointTranslation(); - const solution: string = await leetCodeExecutor.showSolution(problemInput, language, needTranslation); - leetCodeSolutionProvider.show(unescapeJS(solution)); - } catch (error) { - leetCodeChannel.appendLine(error.toString()); - await promptForOpenOutputChannel("Failed to fetch the top voted solution. Please open the output channel for details.", DialogType.error); - } + const language: string | undefined = await fetchProblemLanguage(); + if (!language) { + return; + } + try { + const needTranslation: boolean = + settingUtils.shouldUseEndpointTranslation(); + const solution: string = await leetCodeExecutor.showSolution( + problemInput, + language, + needTranslation + ); + leetCodeSolutionProvider.show(unescapeJS(solution)); + } catch (error) { + leetCodeChannel.appendLine(error.toString()); + await promptForOpenOutputChannel( + "Failed to fetch the top voted solution. Please open the output channel for details.", + DialogType.error + ); + } } 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 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", + ignoreFocusOut: true, + })); + // fire-and-forget default language query + (async (): Promise => { + if ( + language && + !defaultLanguage && + leetCodeConfig.get("hint.setDefaultLanguage") + ) { + 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( + "hint.setDefaultLanguage", + false, + true /* UserSetting */ + ); + } } - const language: string | undefined = defaultLanguage || await vscode.window.showQuickPick(languages, { placeHolder: "Select the language you want to use", ignoreFocusOut: true }); - // fire-and-forget default language query - (async (): Promise => { - if (language && !defaultLanguage && leetCodeConfig.get("hint.setDefaultLanguage")) { - 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("hint.setDefaultLanguage", false, true /* UserSetting */); - } - } - })(); - return language; + })(); + return language; } async function showProblemInternal(node: IProblem): Promise { - try { - const language: string | undefined = await fetchProblemLanguage(); - if (!language) { - return; - } - - const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode"); - const workspaceFolder: string = await selectWorkspaceFolder(); - if (!workspaceFolder) { - return; - } + try { + const language: string | undefined = await fetchProblemLanguage(); + if (!language) { + return; + } - const fileFolder: string = leetCodeConfig - .get(`filePath.${language}.folder`, leetCodeConfig.get(`filePath.default.folder`, "")) - .trim(); - const fileName: string = leetCodeConfig - .get( - `filePath.${language}.filename`, - leetCodeConfig.get(`filePath.default.filename`) || genFileName(node, language), - ) - .trim(); + const leetCodeConfig: vscode.WorkspaceConfiguration = + vscode.workspace.getConfiguration("leetcode"); + const workspaceFolder: string = await selectWorkspaceFolder(); + if (!workspaceFolder) { + return; + } - let finalPath: string = path.join(workspaceFolder, fileFolder, fileName); + const fileFolder: string = leetCodeConfig + .get( + `filePath.${language}.folder`, + leetCodeConfig.get(`filePath.default.folder`, "") + ) + .trim(); + const fileName: string = leetCodeConfig + .get( + `filePath.${language}.filename`, + leetCodeConfig.get(`filePath.default.filename`) || + genFileName(node, language) + ) + .trim(); - if (finalPath) { - finalPath = await resolveRelativePath(finalPath, node, language); - if (!finalPath) { - leetCodeChannel.appendLine("Showing problem canceled by user."); - return; - } - } + let finalPath: string = path.join(workspaceFolder, fileFolder, fileName); - finalPath = wsl.useWsl() ? await wsl.toWinPath(finalPath) : finalPath; + if (finalPath) { + finalPath = await resolveRelativePath(finalPath, node, language); + if (!finalPath) { + leetCodeChannel.appendLine("Showing problem canceled by user."); + return; + } + } - const descriptionConfig: IDescriptionConfiguration = settingUtils.getDescriptionConfiguration(); - const needTranslation: boolean = settingUtils.shouldUseEndpointTranslation(); + finalPath = wsl.useWsl() ? await wsl.toWinPath(finalPath) : finalPath; - await leetCodeExecutor.showProblem(node, language, finalPath, descriptionConfig.showInComment, needTranslation); - const promises: any[] = [ - vscode.window.showTextDocument(vscode.Uri.file(finalPath), { preview: false, viewColumn: vscode.ViewColumn.One }), - promptHintMessage( - "hint.commentDescription", - 'You can config how to show the problem description through "leetcode.showDescription".', - "Open settings", - (): Promise => openSettingsEditor("leetcode.showDescription"), - ), - ]; - if (descriptionConfig.showInWebview) { - promises.push(showDescriptionView(node)); - } + const descriptionConfig: IDescriptionConfiguration = + settingUtils.getDescriptionConfiguration(); + const needTranslation: boolean = + settingUtils.shouldUseEndpointTranslation(); - await Promise.all(promises); - } catch (error) { - await promptForOpenOutputChannel(`${error} Please open the output channel for details.`, DialogType.error); + await leetCodeExecutor.showProblem( + node, + language, + finalPath, + descriptionConfig.showInComment, + needTranslation + ); + const promises: any[] = [ + vscode.window.showTextDocument(vscode.Uri.file(finalPath), { + preview: false, + viewColumn: vscode.ViewColumn.Two, + }), + promptHintMessage( + "hint.commentDescription", + 'You can config how to show the problem description through "leetcode.showDescription".', + "Open settings", + (): Promise => openSettingsEditor("leetcode.showDescription") + ), + ]; + if (descriptionConfig.showInWebview) { + promises.push(showDescriptionView(node)); } + + await Promise.all(promises); + } catch (error) { + await promptForOpenOutputChannel( + `${error} Please open the output channel for details.`, + DialogType.error + ); + } } async function showDescriptionView(node: IProblem): Promise { - return previewProblem(node, vscode.workspace.getConfiguration("leetcode").get("enableSideMode", true)); + return previewProblem( + node, + vscode.workspace + .getConfiguration("leetcode") + .get("enableSideMode", true) + ); } -async function parseProblemsToPicks(p: Promise): Promise>> { - return new Promise(async (resolve: (res: Array>) => void): Promise => { - 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}`, - value: problem, - })); - resolve(picks); - }); +async function parseProblemsToPicks( + p: Promise +): Promise>> { + return new Promise( + async ( + resolve: (res: Array>) => void + ): Promise => { + 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}`, + value: problem, + } + ) + ); + resolve(picks); + } + ); } function parseProblemDecorator(state: ProblemState, locked: boolean): string { - switch (state) { - case ProblemState.AC: - return "$(check) "; - case ProblemState.NotAC: - return "$(x) "; - default: - return locked ? "$(lock) " : ""; - } + switch (state) { + case ProblemState.AC: + return "$(check) "; + case ProblemState.NotAC: + return "$(x) "; + default: + return locked ? "$(lock) " : ""; + } } -async function resolveRelativePath(relativePath: string, node: IProblem, selectedLanguage: string): Promise { - let tag: string = ""; - if (/\$\{tag\}/i.test(relativePath)) { - tag = (await resolveTagForProblem(node)) || ""; - } +async function resolveRelativePath( + relativePath: string, + node: IProblem, + selectedLanguage: string +): Promise { + let tag: string = ""; + if (/\$\{tag\}/i.test(relativePath)) { + tag = (await resolveTagForProblem(node)) || ""; + } - let company: string = ""; - if (/\$\{company\}/i.test(relativePath)) { - company = (await resolveCompanyForProblem(node)) || ""; - } + let company: string = ""; + if (/\$\{company\}/i.test(relativePath)) { + company = (await resolveCompanyForProblem(node)) || ""; + } - return relativePath.replace(/\$\{(.*?)\}/g, (_substring: string, ...args: string[]) => { - const placeholder: string = args[0].toLowerCase().trim(); - switch (placeholder) { - case "id": - return node.id; - case "name": - return node.name; - case "camelcasename": - return _.camelCase(node.name); - case "pascalcasename": - return _.upperFirst(_.camelCase(node.name)); - case "kebabcasename": - case "kebab-case-name": - return _.kebabCase(node.name); - case "snakecasename": - case "snake_case_name": - return _.snakeCase(node.name); - case "ext": - return genFileExt(selectedLanguage); - case "language": - return selectedLanguage; - case "difficulty": - return node.difficulty.toLocaleLowerCase(); - case "tag": - return tag; - case "company": - return company; - default: - const errorMsg: string = `The config '${placeholder}' is not supported.`; - leetCodeChannel.appendLine(errorMsg); - throw new Error(errorMsg); - } - }); + return relativePath.replace( + /\$\{(.*?)\}/g, + (_substring: string, ...args: string[]) => { + const placeholder: string = args[0].toLowerCase().trim(); + switch (placeholder) { + case "id": + return node.id; + case "name": + return node.name; + case "camelcasename": + return _.camelCase(node.name); + case "pascalcasename": + return _.upperFirst(_.camelCase(node.name)); + case "kebabcasename": + case "kebab-case-name": + return _.kebabCase(node.name); + case "snakecasename": + case "snake_case_name": + return _.snakeCase(node.name); + case "ext": + return genFileExt(selectedLanguage); + case "language": + return selectedLanguage; + case "difficulty": + return node.difficulty.toLocaleLowerCase(); + case "tag": + return tag; + case "company": + return company; + default: + const errorMsg: string = `The config '${placeholder}' is not supported.`; + leetCodeChannel.appendLine(errorMsg); + throw new Error(errorMsg); + } + } + ); } -async function resolveTagForProblem(problem: IProblem): Promise { - if (problem.tags.length === 1) { - return problem.tags[0]; - } - return await vscode.window.showQuickPick( - problem.tags, - { - matchOnDetail: true, - placeHolder: "Multiple tags available, please select one", - ignoreFocusOut: true, - }, - ); +async function resolveTagForProblem( + problem: IProblem +): Promise { + if (problem.tags.length === 1) { + return problem.tags[0]; + } + return await vscode.window.showQuickPick(problem.tags, { + matchOnDetail: true, + placeHolder: "Multiple tags available, please select one", + ignoreFocusOut: true, + }); } -async function resolveCompanyForProblem(problem: IProblem): Promise { - if (problem.companies.length === 1) { - return problem.companies[0]; - } - return await vscode.window.showQuickPick(problem.companies, { - matchOnDetail: true, - placeHolder: "Multiple tags available, please select one", - ignoreFocusOut: true, - }); +async function resolveCompanyForProblem( + problem: IProblem +): Promise { + if (problem.companies.length === 1) { + return problem.companies[0]; + } + return await vscode.window.showQuickPick(problem.companies, { + matchOnDetail: true, + placeHolder: "Multiple tags available, please select one", + ignoreFocusOut: true, + }); } diff --git a/src/webview/LeetCodeWebview.ts b/src/webview/LeetCodeWebview.ts index 8532c8d..ebb3575 100644 --- a/src/webview/LeetCodeWebview.ts +++ b/src/webview/LeetCodeWebview.ts @@ -1,82 +1,108 @@ // Copyright (c) jdneo. All rights reserved. // Licensed under the MIT license. -import { commands, ConfigurationChangeEvent, Disposable, ViewColumn, WebviewPanel, window, workspace } from "vscode"; +import { + commands, + ConfigurationChangeEvent, + Disposable, + ViewColumn, + WebviewPanel, + window, + workspace, +} from "vscode"; import { openSettingsEditor, promptHintMessage } from "../utils/uiUtils"; import { markdownEngine } from "./markdownEngine"; export abstract class LeetCodeWebview implements Disposable { + protected readonly viewType: string = "leetcode.webview"; + protected panel: WebviewPanel | undefined; + private listeners: Disposable[] = []; - protected readonly viewType: string = "leetcode.webview"; - protected panel: WebviewPanel | undefined; - private listeners: Disposable[] = []; - - public dispose(): void { - if (this.panel) { - this.panel.dispose(); - } + public dispose(): void { + if (this.panel) { + this.panel.dispose(); } + } - protected showWebviewInternal(): void { - const { title, viewColumn, preserveFocus } = this.getWebviewOption(); - if (!this.panel) { - this.panel = window.createWebviewPanel(this.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; - if (viewColumn === ViewColumn.Two) { - // Make sure second group exists. See vscode#71608 issue - commands.executeCommand("workbench.action.focusSecondEditorGroup").then(() => { - this.panel!.reveal(viewColumn, preserveFocus); - }); - } else { - this.panel.reveal(viewColumn, preserveFocus); - } + protected showWebviewInternal(): void { + const { title, viewColumn, preserveFocus } = this.getWebviewOption(); + if (!this.panel) { + this.panel = window.createWebviewPanel( + this.viewType, + title, + { viewColumn: 1, preserveFocus }, + { + enableScripts: true, + enableCommandUris: true, + enableFindWidget: true, + retainContextWhenHidden: true, + localResourceRoots: markdownEngine.localResourceRoots, } - this.panel.webview.html = this.getWebviewContent(); - this.showMarkdownConfigHint(); + ); + 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; + if (viewColumn === ViewColumn.Two) { + // Make sure second group exists. See vscode#71608 issue + commands + .executeCommand("workbench.action.focusSecondEditorGroup") + .then(() => { + this.panel!.reveal(1, true); + }); + } else { + this.panel.reveal(1, true); + } } + this.panel.webview.html = this.getWebviewContent(); + this.showMarkdownConfigHint(); + } - protected onDidDisposeWebview(): void { - this.panel = undefined; - for (const listener of this.listeners) { - listener.dispose(); - } - this.listeners = []; + 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 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 async onDidReceiveMessage(_message: any): Promise { + /* no special rule */ + } - protected abstract getWebviewOption(): ILeetCodeWebviewOption; + protected abstract getWebviewOption(): ILeetCodeWebviewOption; - protected abstract getWebviewContent(): string; + protected abstract getWebviewContent(): string; - private async showMarkdownConfigHint(): Promise { - await promptHintMessage( - "hint.configWebviewMarkdown", - 'You can change the webview appearance ("fontSize", "lineWidth" & "fontFamily") in "markdown.preview" configuration.', - "Open settings", - (): Promise => openSettingsEditor("markdown.preview"), - ); - } + private async showMarkdownConfigHint(): Promise { + await promptHintMessage( + "hint.configWebviewMarkdown", + 'You can change the webview appearance ("fontSize", "lineWidth" & "fontFamily") in "markdown.preview" configuration.', + "Open settings", + (): Promise => openSettingsEditor("markdown.preview") + ); + } } export interface ILeetCodeWebviewOption { - title: string; - viewColumn: ViewColumn; - preserveFocus?: boolean; + title: string; + viewColumn: ViewColumn; + preserveFocus?: boolean; }