diff --git a/src/commands/list.ts b/src/commands/list.ts index fd425796..4656a230 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -1,9 +1,9 @@ "use strict"; import * as vscode from "vscode"; +import { leetCodeExecutor } from "../leetCodeExecutor"; import { leetCodeManager } from "../leetCodeManager"; -import { leetCodeBinaryPath, ProblemState, UserStatus } from "../shared"; -import { executeCommand } from "../utils/cpUtils"; +import { ProblemState, UserStatus } from "../shared"; import { DialogType, promptForOpenOutputChannel } from "../utils/uiUtils"; export interface IProblem { @@ -22,8 +22,8 @@ export async function listProblems(): Promise { return []; } const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode"); - const showLocked: boolean | undefined = leetCodeConfig.get("showLocked"); - const result: string = await executeCommand("node", showLocked ? [leetCodeBinaryPath, "list"] : [leetCodeBinaryPath, "list", "-q", "L"]); + const showLocked: boolean = !!leetCodeConfig.get("showLocked"); + const result: string = await leetCodeExecutor.listProblems(showLocked); const problems: IProblem[] = []; const lines: string[] = result.split("\n"); const reg: RegExp = /^(.)\s(.{1,2})\s(.)\s\[\s*(\d*)\]\s*(.*)\s*(Easy|Medium|Hard)\s*\((\s*\d+\.\d+ %)\)/; diff --git a/src/commands/session.ts b/src/commands/session.ts index b3f3e431..04da3239 100644 --- a/src/commands/session.ts +++ b/src/commands/session.ts @@ -1,9 +1,9 @@ "use strict"; import * as vscode from "vscode"; +import { leetCodeExecutor } from "../leetCodeExecutor"; import { leetCodeManager } from "../leetCodeManager"; -import { IQuickItemEx, leetCodeBinaryPath } from "../shared"; -import { executeCommand } from "../utils/cpUtils"; +import { IQuickItemEx } from "../shared"; import { DialogType, promptForOpenOutputChannel, promptForSignIn } from "../utils/uiUtils"; export async function getSessionList(): Promise { @@ -12,7 +12,7 @@ export async function getSessionList(): Promise { promptForSignIn(); return []; } - const result: string = await executeCommand("node", [leetCodeBinaryPath, "session"]); + const result: string = await leetCodeExecutor.listSessions(); const lines: string[] = result.split("\n"); const sessions: ISession[] = []; const reg: RegExp = /(.?)\s*(\d+)\s+(.*)\s+(\d+ \(\s*\d+\.\d+ %\))\s+(\d+ \(\s*\d+\.\d+ %\))/; @@ -41,7 +41,7 @@ export async function selectSession(): Promise { return; } try { - await executeCommand("node", [leetCodeBinaryPath, "session", "-e", choice.value]); + await leetCodeExecutor.enableSession(choice.value); vscode.window.showInformationMessage(`Successfully switched to session '${choice.label}'.`); await vscode.commands.executeCommand("leetcode.refreshExplorer"); } catch (error) { @@ -81,7 +81,7 @@ export async function createSession(): Promise { return; } try { - await executeCommand("node", [leetCodeBinaryPath, "session", "-c", session]); + await leetCodeExecutor.createSession(session); vscode.window.showInformationMessage("New session created, you can switch to it by clicking the status bar."); } catch (error) { await promptForOpenOutputChannel("Failed to create session. Please open the output channel for details.", DialogType.error); diff --git a/src/commands/show.ts b/src/commands/show.ts index 808f00be..e50fa821 100644 --- a/src/commands/show.ts +++ b/src/commands/show.ts @@ -2,10 +2,10 @@ import * as fse from "fs-extra"; import * as vscode from "vscode"; +import { leetCodeExecutor } from "../leetCodeExecutor"; import { LeetCodeNode } from "../leetCodeExplorer"; import { leetCodeManager } from "../leetCodeManager"; -import { IQuickItemEx, languages, leetCodeBinaryPath, ProblemState } from "../shared"; -import { executeCommandWithProgress } from "../utils/cpUtils"; +import { IQuickItemEx, languages, ProblemState } from "../shared"; import { DialogOptions, DialogType, promptForOpenOutputChannel, promptForSignIn } from "../utils/uiUtils"; import { selectWorkspaceFolder } from "../utils/workspaceUtils"; import * as wsl from "../utils/wslUtils"; @@ -50,11 +50,11 @@ async function showProblemInternal(id: string): Promise { const outdir: string = await selectWorkspaceFolder(); await fse.ensureDir(outdir); - const result: string = await executeCommandWithProgress("Fetching problem data...", "node", [leetCodeBinaryPath, "show", id, "-gx", "-l", language, "-o", `"${outdir}"`]); + const result: string = await leetCodeExecutor.showProblem(id, language, outdir); const reg: RegExp = /\* Source Code:\s*(.*)/; const match: RegExpMatchArray | null = result.match(reg); if (match && match.length >= 2) { - const filePath: string = wsl.useWsl() ? wsl.toWinPath(match[1].trim()) : match[1].trim(); + const filePath: string = wsl.useWsl() ? await wsl.toWinPath(match[1].trim()) : match[1].trim(); await vscode.window.showTextDocument(vscode.Uri.file(filePath), { preview: false }); } else { diff --git a/src/commands/submit.ts b/src/commands/submit.ts index ee67720a..1eaeb2b3 100644 --- a/src/commands/submit.ts +++ b/src/commands/submit.ts @@ -1,9 +1,8 @@ "use strict"; import * as vscode from "vscode"; +import { leetCodeExecutor } from "../leetCodeExecutor"; import { leetCodeManager } from "../leetCodeManager"; -import { leetCodeBinaryPath } from "../shared"; -import { executeCommandWithProgress } from "../utils/cpUtils"; import { DialogType, promptForOpenOutputChannel, promptForSignIn, showResultFile } from "../utils/uiUtils"; import { getActivefilePath } from "../utils/workspaceUtils"; @@ -19,7 +18,7 @@ export async function submitSolution(uri?: vscode.Uri): Promise { } try { - const result: string = await executeCommandWithProgress("Submitting to LeetCode...", "node", [leetCodeBinaryPath, "submit", `"${filePath}"`]); + const result: string = await leetCodeExecutor.submitSolution(filePath); await showResultFile(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 2df1878e..4d22b5e9 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -2,9 +2,9 @@ import * as fse from "fs-extra"; import * as vscode from "vscode"; +import { leetCodeExecutor } from "../leetCodeExecutor"; import { leetCodeManager } from "../leetCodeManager"; -import { IQuickItemEx, leetCodeBinaryPath, UserStatus } from "../shared"; -import { executeCommandWithProgress } from "../utils/cpUtils"; +import { IQuickItemEx, UserStatus } from "../shared"; import { DialogType, promptForOpenOutputChannel, showFileSelectDialog, showResultFile } from "../utils/uiUtils"; import { getActivefilePath } from "../utils/workspaceUtils"; @@ -47,7 +47,7 @@ export async function testSolution(uri?: vscode.Uri): Promise { let result: string | undefined; switch (choice.value) { case ":default": - result = await executeCommandWithProgress("Submitting to LeetCode...", "node", [leetCodeBinaryPath, "test", `"${filePath}"`]); + result = await leetCodeExecutor.testSolution(filePath); break; case ":direct": const testString: string | undefined = await vscode.window.showInputBox({ @@ -57,7 +57,7 @@ export async function testSolution(uri?: vscode.Uri): Promise { ignoreFocusOut: true, }); if (testString) { - result = await executeCommandWithProgress("Submitting to LeetCode...", "node", [leetCodeBinaryPath, "test", `"${filePath}"`, "-t", `"${testString.replace(/"/g, "")}"`]); + result = await leetCodeExecutor.testSolution(filePath, testString.replace(/"/g, "")); } break; case ":file": @@ -65,7 +65,7 @@ export async function testSolution(uri?: vscode.Uri): Promise { if (testFile && testFile.length) { const input: string = await fse.readFile(testFile[0].fsPath, "utf-8"); if (input.trim()) { - result = await executeCommandWithProgress("Submitting to LeetCode...", "node", [leetCodeBinaryPath, "test", `"${filePath}"`, "-t", `"${input.replace(/"/g, "").replace(/\r?\n/g, "\\n")}"`]); + result = await leetCodeExecutor.testSolution(filePath, input.replace(/"/g, "").replace(/\r?\n/g, "\\n")); } else { vscode.window.showErrorMessage("The selected test file must not be empty."); } diff --git a/src/extension.ts b/src/extension.ts index b8b298b9..78182b67 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,13 +6,13 @@ import * as show from "./commands/show"; import * as submit from "./commands/submit"; import * as test from "./commands/test"; import { leetCodeChannel } from "./leetCodeChannel"; +import { leetCodeExecutor } from "./leetCodeExecutor"; import { LeetCodeNode, LeetCodeTreeDataProvider } from "./leetCodeExplorer"; import { leetCodeManager } from "./leetCodeManager"; import { leetCodeStatusBarItem } from "./leetCodeStatusBarItem"; -import { isNodeInstalled } from "./utils/nodeUtils"; export async function activate(context: vscode.ExtensionContext): Promise { - if (!await isNodeInstalled()) { + if (!await leetCodeExecutor.meetRequirements()) { return; } leetCodeManager.getLoginStatus(); diff --git a/src/leetCodeExecutor.ts b/src/leetCodeExecutor.ts new file mode 100644 index 00000000..20353c03 --- /dev/null +++ b/src/leetCodeExecutor.ts @@ -0,0 +1,127 @@ +"use strict"; + +import * as cp from "child_process"; +import * as opn from "opn"; +import * as path from "path"; +import * as vscode from "vscode"; +import { executeCommand, executeCommandWithProgress } from "./utils/cpUtils"; +import { DialogOptions } from "./utils/uiUtils"; +import * as wsl from "./utils/wslUtils"; + +export interface ILeetCodeExecutor { + meetRequirements(): Promise; + getLeetCodeBinaryPath(): Promise; + + /* section for user command */ + getUserInfo(): Promise; + signOut(): Promise; + // TODO: implement login when leetcode-cli support login in batch mode. + // signIn(): Promise; + + /* section for problem command */ + listProblems(showLocked: boolean): Promise; + showProblem(id: string, language: string, outdir: string): Promise; + + /* section for session command */ + listSessions(): Promise; + enableSession(name: string): Promise; + createSession(name: string): Promise; + + /* section for solution command */ + submitSolution(filePath: string): Promise; + testSolution(filePath: string, testString?: string): Promise; +} + +class LeetCodeExecutor implements ILeetCodeExecutor { + private leetCodeBinaryPath: string; + private leetCodeBinaryPathInWsl: string; + + constructor() { + this.leetCodeBinaryPath = path.join(__dirname, "..", "..", "node_modules", "leetcode-cli", "bin", "leetcode"); + this.leetCodeBinaryPathInWsl = ""; + } + + public async getLeetCodeBinaryPath(): Promise { + if (wsl.useWsl()) { + if (!this.leetCodeBinaryPathInWsl) { + this.leetCodeBinaryPathInWsl = `${await wsl.toWslPath(this.leetCodeBinaryPath)}`; + } + return `"${this.leetCodeBinaryPathInWsl}"`; + } + return `"${this.leetCodeBinaryPath}"`; + } + + 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", + DialogOptions.open, + ); + if (choice === DialogOptions.open) { + opn("https://nodejs.org"); + } + return false; + } + } + + public async getUserInfo(): Promise { + return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "user"]); + } + + public async signOut(): Promise { + return await await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "user", "-L"]); + } + + public async listProblems(showLocked: boolean): Promise { + return await this.executeCommandEx("node", showLocked ? + [await this.getLeetCodeBinaryPath(), "list"] : + [await this.getLeetCodeBinaryPath(), "list", "-q", "L"], + ); + } + + public async showProblem(id: string, language: string, outdir: string): Promise { + return await this.executeCommandWithProgressEx("Fetching problem data...", "node", [await this.getLeetCodeBinaryPath(), "show", id, "-gx", "-l", language, "-o", `"${outdir}"`]); + } + + public async listSessions(): Promise { + return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "session"]); + } + + public async enableSession(name: string): Promise { + return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "session", "-e", name]); + } + + public async createSession(name: string): Promise { + return await this.executeCommandEx("node", [await this.getLeetCodeBinaryPath(), "session", "-c", name]); + } + + public async submitSolution(filePath: string): Promise { + return await this.executeCommandWithProgressEx("Submitting to LeetCode...", "node", [await this.getLeetCodeBinaryPath(), "submit", `"${filePath}"`]); + } + + public async testSolution(filePath: string, testString?: string): Promise { + if (testString) { + return await this.executeCommandWithProgressEx("Submitting to LeetCode...", "node", [await this.getLeetCodeBinaryPath(), "test", `"${filePath}"`, "-t", `"${testString}"`]); + } + return await this.executeCommandWithProgressEx("Submitting to LeetCode...", "node", [await this.getLeetCodeBinaryPath(), "test", `"${filePath}"`]); + } + + private async executeCommandEx(command: string, args: string[], options: cp.SpawnOptions = { shell: true }): Promise { + if (wsl.useWsl()) { + return await executeCommand("wsl", [command].concat(args), options); + } + return await executeCommand(command, args, options); + } + + private async executeCommandWithProgressEx(message: string, command: string, args: string[], options: cp.SpawnOptions = { shell: true }): Promise { + if (wsl.useWsl()) { + return await executeCommandWithProgress(message, "wsl", [command].concat(args), options); + } + return await executeCommandWithProgress(message, command, args, options); + } +} + +export const leetCodeExecutor: ILeetCodeExecutor = new LeetCodeExecutor(); diff --git a/src/leetCodeManager.ts b/src/leetCodeManager.ts index 542b365d..24ad4ebc 100644 --- a/src/leetCodeManager.ts +++ b/src/leetCodeManager.ts @@ -4,9 +4,8 @@ import * as cp from "child_process"; import { EventEmitter } from "events"; import * as vscode from "vscode"; import { leetCodeChannel } from "./leetCodeChannel"; +import { leetCodeExecutor } from "./leetCodeExecutor"; import { UserStatus } from "./shared"; -import { leetCodeBinaryPath } from "./shared"; -import { executeCommand } from "./utils/cpUtils"; import { DialogType, promptForOpenOutputChannel } from "./utils/uiUtils"; import * as wsl from "./utils/wslUtils"; @@ -30,7 +29,7 @@ class LeetCodeManager extends EventEmitter implements ILeetCodeManager { public async getLoginStatus(): Promise { try { - const result: string = await executeCommand("node", [leetCodeBinaryPath, "user"]); + const result: string = await leetCodeExecutor.getUserInfo(); this.currentUser = result.slice("You are now login as".length).trim(); this.userStatus = UserStatus.SignedIn; } catch (error) { @@ -46,6 +45,8 @@ class LeetCodeManager extends EventEmitter implements ILeetCodeManager { const userName: string | undefined = await new Promise(async (resolve: (res: string | undefined) => void, reject: (e: Error) => void): Promise => { let result: string = ""; + const leetCodeBinaryPath: string = await leetCodeExecutor.getLeetCodeBinaryPath(); + const childProc: cp.ChildProcess = wsl.useWsl() ? cp.spawn("wsl", ["node", leetCodeBinaryPath, "user", "-l"], { shell: true }) : cp.spawn("node", [leetCodeBinaryPath, "user", "-l"], { shell: true }); @@ -102,7 +103,7 @@ class LeetCodeManager extends EventEmitter implements ILeetCodeManager { public async signOut(): Promise { try { - await executeCommand("node", [leetCodeBinaryPath, "user", "-L"]); + await leetCodeExecutor.signOut(); vscode.window.showInformationMessage("Successfully signed out."); this.currentUser = undefined; this.userStatus = UserStatus.SignedOut; diff --git a/src/shared.ts b/src/shared.ts index 0c85a68b..9a5184ee 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -1,16 +1,6 @@ "use strict"; -import * as path from "path"; import * as vscode from "vscode"; -import * as wsl from "./utils/wslUtils"; - -let binPath: string = path.join(__dirname, "..", "..", "node_modules", "leetcode-cli", "bin", "leetcode"); - -if (wsl.useWsl()) { - binPath = wsl.toWslPath(binPath); -} - -export const leetCodeBinaryPath: string = `"${binPath}"`; export interface IQuickItemEx extends vscode.QuickPickItem { value: T; diff --git a/src/utils/cpUtils.ts b/src/utils/cpUtils.ts index c8393ebb..75e7f6ce 100644 --- a/src/utils/cpUtils.ts +++ b/src/utils/cpUtils.ts @@ -3,15 +3,12 @@ import * as cp from "child_process"; import * as vscode from "vscode"; import { leetCodeChannel } from "../leetCodeChannel"; -import * as wsl from "./wslUtils"; export async function executeCommand(command: string, args: string[], options: cp.SpawnOptions = { shell: true }): Promise { return new Promise((resolve: (res: string) => void, reject: (e: Error) => void): void => { let result: string = ""; - const childProc: cp.ChildProcess = wsl.useWsl() - ? cp.spawn("wsl", [command].concat(args), options) - : cp.spawn(command, args, options); + const childProc: cp.ChildProcess = cp.spawn(command, args, options); childProc.stdout.on("data", (data: string | Buffer) => { data = data.toString(); diff --git a/src/utils/nodeUtils.ts b/src/utils/nodeUtils.ts deleted file mode 100644 index 98df8705..00000000 --- a/src/utils/nodeUtils.ts +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; - -import * as opn from "opn"; -import * as vscode from "vscode"; -import { executeCommand } from "./cpUtils"; -import { DialogOptions } from "./uiUtils"; - -export async function isNodeInstalled(): Promise { - try { - await executeCommand("node", ["-v"]); - return true; - } catch (error) { - const choice: vscode.MessageItem | undefined = await vscode.window.showErrorMessage( - "LeetCode extension need Node.js installed in environment path", - DialogOptions.open, - ); - if (choice === DialogOptions.open) { - opn("https://nodejs.org"); - } - return false; - } -} diff --git a/src/utils/wslUtils.ts b/src/utils/wslUtils.ts index d2b0d566..6b1efcc3 100644 --- a/src/utils/wslUtils.ts +++ b/src/utils/wslUtils.ts @@ -1,18 +1,17 @@ "use strict"; -import * as cp from "child_process"; import * as vscode from "vscode"; +import { executeCommand } from "./cpUtils"; export function useWsl(): boolean { const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode"); - return process.platform === "win32" && leetCodeConfig.get("useWsl") === true; } -export function toWslPath(path: string): string { - return cp.execFileSync("wsl", ["wslpath", "-u", `${path.replace(/\\/g, "/")}`]).toString().trim(); +export async function toWslPath(path: string): Promise { + return (await executeCommand("wsl", ["wslpath", "-u", `"${path.replace(/\\/g, "/")}"`])).trim(); } -export function toWinPath(path: string): string { - return cp.execFileSync("wsl", ["wslpath", "-w", path]).toString().trim(); +export async function toWinPath(path: string): Promise { + return (await executeCommand("wsl", ["wslpath", "-w", `"${path}"`])).trim(); }