diff --git a/package.json b/package.json index 7e759e68..0ffde67d 100644 --- a/package.json +++ b/package.json @@ -367,7 +367,248 @@ "leetcode.outputFolder": { "type": "string", "scope": "application", - "description": "The relative path to save the problem files." + "description": "[Deprecated] The output folder to save the problem files." + }, + "leetcode.filePath": { + "type": "object", + "scope": "application", + "description": "The output folder and filename to save the problem files.", + "properties": { + "default": { + "type": "object", + "properties": { + "folder": { + "type": "string", + "examples": [ + "src" + ] + }, + "filename": { + "type": "string", + "examples": [ + "${camelCaseName}.${ext}", + "${PascalCaseName}.${ext}", + "${id}-${kebab-case-name}.${ext}", + "${id}_${snake_case_name}.${ext}" + ] + } + }, + "required": [ + "folder", + "filename" + ] + }, + "bash": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "c": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "cpp": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "csharp": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "golang": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "java": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "javascript": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "kotlin": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "mysql": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "php": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "python": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "python3": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "ruby": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "rust": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "scala": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "swift": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + } + }, + "additionalProperties": { + "type": "object", + "properties": { + "folder": { + "type": "string" + }, + "filename": { + "type": "string" + } + }, + "minProperties": 1 + }, + "default": { + "default": { + "folder": "", + "filename": "${id}.${kebab-case-name}.${ext}" + } + } }, "leetcode.enableStatusBar": { "type": "boolean", diff --git a/src/commands/show.ts b/src/commands/show.ts index b946c298..1906bd8e 100644 --- a/src/commands/show.ts +++ b/src/commands/show.ts @@ -1,7 +1,7 @@ // Copyright (c) jdneo. All rights reserved. // Licensed under the MIT license. -import * as fse from "fs-extra"; +import * as _ from "lodash"; import * as path from "path"; import * as unescapeJS from "unescape-js"; import * as vscode from "vscode"; @@ -11,7 +11,7 @@ import { leetCodeChannel } from "../leetCodeChannel"; import { leetCodeExecutor } from "../leetCodeExecutor"; import { leetCodeManager } from "../leetCodeManager"; import { IProblem, IQuickItemEx, languages, ProblemState } from "../shared"; -import { getNodeIdFromFile } from "../utils/problemUtils"; +import { genFileExt, genFileName, getNodeIdFromFile } from "../utils/problemUtils"; import { DialogOptions, DialogType, openSettingsEditor, promptForOpenOutputChannel, promptForSignIn, promptHintMessage } from "../utils/uiUtils"; import { getActiveFilePath, selectWorkspaceFolder } from "../utils/workspaceUtils"; import * as wsl from "../utils/wslUtils"; @@ -137,27 +137,38 @@ async function showProblemInternal(node: IProblem): Promise { } const leetCodeConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("leetcode"); - let outDir: string = await selectWorkspaceFolder(); - if (!outDir) { + const workspaceFolder: string = await selectWorkspaceFolder(); + if (!workspaceFolder) { return; } - let relativePath: string = (leetCodeConfig.get("outputFolder", "")).trim(); - if (relativePath) { - relativePath = await resolveRelativePath(relativePath, node, language); - if (!relativePath) { + const outputFolder: string = leetCodeConfig.get("outputFolder", "").trim(); + + const fileFolder: string = leetCodeConfig + .get(`filePath.${language}.folder`, leetCodeConfig.get(`filePath.default.folder`, outputFolder)) + .trim(); + const fileName: string = leetCodeConfig + .get( + `filePath.${language}.filename`, + leetCodeConfig.get(`filePath.default.filename`, genFileName(node, language)), + ) + .trim(); + + let finalPath: string = path.join(workspaceFolder, fileFolder, fileName); + + if (finalPath) { + finalPath = await resolveRelativePath(finalPath, node, language); + if (!finalPath) { leetCodeChannel.appendLine("Showing problem canceled by user."); return; } } - outDir = path.join(outDir, relativePath); - await fse.ensureDir(outDir); + finalPath = wsl.useWsl() ? await wsl.toWinPath(finalPath) : finalPath; - const originFilePath: string = await leetCodeExecutor.showProblem(node, language, outDir, leetCodeConfig.get("showCommentDescription")); - const filePath: string = wsl.useWsl() ? await wsl.toWinPath(originFilePath) : originFilePath; + await leetCodeExecutor.showProblem(node, language, finalPath, leetCodeConfig.get("showCommentDescription")); await Promise.all([ - vscode.window.showTextDocument(vscode.Uri.file(filePath), { preview: false, viewColumn: vscode.ViewColumn.One }), + vscode.window.showTextDocument(vscode.Uri.file(finalPath), { preview: false, viewColumn: vscode.ViewColumn.One }), movePreviewAsideIfNeeded(node), promptHintMessage( "hint.commentDescription", @@ -201,26 +212,49 @@ function parseProblemDecorator(state: ProblemState, locked: boolean): string { } async function resolveRelativePath(relativePath: string, node: IProblem, selectedLanguage: string): Promise { + let tag: string = ""; if (/\$\{tag\}/i.test(relativePath)) { - const tag: string | undefined = await resolveTagForProblem(node); - if (!tag) { - return ""; - } - relativePath = relativePath.replace(/\$\{tag\}/ig, tag); + tag = (await resolveTagForProblem(node)) || ""; } - relativePath = relativePath.replace(/\$\{language\}/ig, selectedLanguage); - relativePath = relativePath.replace(/\$\{difficulty\}/ig, node.difficulty.toLocaleLowerCase()); - - // Check if there is any unsupported configuration - const matchResult: RegExpMatchArray | null = relativePath.match(/\$\{(.*?)\}/); - if (matchResult && matchResult.length >= 1) { - const errorMsg: string = `The config '${matchResult[1]}' is not supported.`; - leetCodeChannel.appendLine(errorMsg); - throw new Error(errorMsg); + let company: string = ""; + if (/\$\{company\}/i.test(relativePath)) { + company = (await resolveCompanyForProblem(node)) || ""; } - return relativePath; + 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 { @@ -236,3 +270,14 @@ async function resolveTagForProblem(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/leetCodeExecutor.ts b/src/leetCodeExecutor.ts index 04b89013..ce4a84e8 100644 --- a/src/leetCodeExecutor.ts +++ b/src/leetCodeExecutor.ts @@ -8,7 +8,6 @@ import * as requireFromString from "require-from-string"; import { ConfigurationChangeEvent, Disposable, MessageItem, window, workspace, WorkspaceConfiguration } from "vscode"; import { Endpoint, IProblem, supportedPlugins } from "./shared"; import { executeCommand, executeCommandWithProgress } from "./utils/cpUtils"; -import { genFileName } from "./utils/problemUtils"; import { DialogOptions, openUrl } from "./utils/uiUtils"; import * as wsl from "./utils/wslUtils"; import { toWslPath, useWsl } from "./utils/wslUtils"; @@ -96,17 +95,14 @@ class LeetCodeExecutor implements Disposable { ); } - public async showProblem(problemNode: IProblem, language: string, outDir: string, detailed: boolean = false): Promise { - const fileName: string = genFileName(problemNode, language); - const filePath: string = path.join(outDir, fileName); + public async showProblem(problemNode: IProblem, language: string, filePath: string, detailed: boolean = false): Promise { const templateType: string = detailed ? "-cx" : "-c"; if (!await fse.pathExists(filePath)) { + await fse.createFile(filePath); const codeTemplate: string = await this.executeCommandWithProgressEx("Fetching problem data...", this.nodeExecutable, [await this.getLeetCodeBinaryPath(), "show", problemNode.id, templateType, "-l", language]); await fse.writeFile(filePath, codeTemplate); } - - return filePath; } public async showSolution(input: string, language: string): Promise {