diff --git a/.github/workflows/check-ts.yml b/.github/workflows/check-ts.yml index 5ce44be..e35c0f8 100644 --- a/.github/workflows/check-ts.yml +++ b/.github/workflows/check-ts.yml @@ -3,7 +3,7 @@ on: name: Compile TypeScript to check for errors jobs: - deploy: + check-ts: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.prettierrc.json b/.prettierrc.json index 39a51ec..be0684a 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -30,7 +30,8 @@ "*.ts" ], "options": { - "quoteProps": "consistent" + "quoteProps": "consistent", + "trailingComma": "es5" } } ] diff --git a/src/configuration.ts b/src/configuration.ts index b4182f5..d46b8c6 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -11,6 +11,10 @@ import {Rules} from "./rules"; import {logger} from "./logger"; import * as utils from "./utils"; import {ExtensionData} from "./extensionData"; +import {Settings} from "./interfaces/settings"; +import {ExtraSingleLineCommentStyles, LineComment, SingleLineCommentStyle} from "./interfaces/commentStyles"; +import {ExtensionMetaData} from "./interfaces/extensionMetaData"; +import {JsonObject, JsonArray, LanguageId, MultiLineLanguageDefinitions, SingleLineLanguageDefinitions} from "./interfaces/utils"; export class Configuration { /************** @@ -26,46 +30,50 @@ export class Configuration { /** * A key:value Map object of language IDs and their config file paths. */ - private languageConfigFilePaths = new Map(); + private languageConfigFilePaths: Map = new Map(); /** * A key:value Map object of language IDs and their configs. */ - private readonly languageConfigs = new Map(); + private readonly languageConfigs: Map = new Map(); /** - * A key:value Map object of supported language IDs and their single-line style comments. + * A key:value Map object of supported and custom supported language IDs + * and their single-line style comments. * - * @property {string} key Language ID. - * @property {string} value Style of line comment. + * @property {string} - Map key can be either "customSupportedLanguages" + * or "supportedLanguages". + * @property {Map} - Map value is an inner Map object of + * language IDs and their single-line comment styles. */ - private singleLineBlocksMap: Map> = new Map(); + private singleLineBlocksMap: Map<"customSupportedLanguages" | "supportedLanguages", Map> = new Map(); /** * A Map object of an array of supported language IDs for multi-line block comments. * - * @property {string} key - "languages" - * @property {string[]} value - Array of language IDs. + * @property {string} - Map key can be either "customSupportedLanguages" + * or "supportedLanguages". + * @property {LanguageId[]} - Map value is an array of language IDs. */ - private multiLineBlocksMap: Map = new Map(); + private multiLineBlocksMap: Map<"customSupportedLanguages" | "supportedLanguages", LanguageId[]> = new Map(); /** * The directory where the auto-generated language definitions are stored. * @type {string} */ - private readonly autoGeneratedDir = `${__dirname}/../../auto-generated-language-definitions`; + private readonly autoGeneratedDir: string = `${__dirname}/../../auto-generated-language-definitions`; /** * The file path for the single-line language definitions. * @type {string} */ - private readonly singleLineLangDefinitionFilePath = `${this.autoGeneratedDir}/single-line-languages.json`; + private readonly singleLineLangDefinitionFilePath: string = `${this.autoGeneratedDir}/single-line-languages.json`; /** * The file path for the multi-line language definitions. * @type {string} */ - private readonly multiLineLangDefinitionFilePath = `${this.autoGeneratedDir}/multi-line-languages.json`; + private readonly multiLineLangDefinitionFilePath: string = `${this.autoGeneratedDir}/multi-line-languages.json`; /*********** * Methods * @@ -74,6 +82,7 @@ export class Configuration { public constructor() { // Always output extension information to channel on activate. logger.debug(`Extension details:`, this.extensionData.getAll()); + logger.debug(`Extension Discovery Paths:`, this.extensionData.getAllExtensionDiscoveryPaths()); this.findAllLanguageConfigFilePaths(); this.setLanguageConfigDefinitions(); @@ -90,7 +99,7 @@ export class Configuration { * * @returns {vscode.Disposable[]} */ - public configureCommentBlocks() { + public configureCommentBlocks(): vscode.Disposable[] { const disposables: vscode.Disposable[] = []; /** @@ -155,16 +164,18 @@ export class Configuration { * * @returns {vscode.Disposable[]} */ - public registerCommands() { + public registerCommands(): vscode.Disposable[] { const singleLineBlockCommand = vscode.commands.registerTextEditorCommand("auto-comment-blocks.singleLineBlock", (textEditor, edit, args) => { this.handleSingleLineBlock(textEditor, edit); }); + const changeBladeMultiLineBlockCommand = vscode.commands.registerTextEditorCommand( "auto-comment-blocks.changeBladeMultiLineBlock", (textEditor, edit, args) => { this.handleChangeBladeMultiLineBlock(textEditor); } ); + return [singleLineBlockCommand, changeBladeMultiLineBlockCommand]; } @@ -178,28 +189,34 @@ export class Configuration { * If `true`, it returns the comments, if `false` (default), it sets the comments to * the language directly. * + * @returns {vscode.CharacterPair | void} Returns the blade comments if `onStart` is `true`, otherwise nothing. + * */ - public setBladeComments(bladeOverrideComments: boolean, onStart: boolean = false): any { + public setBladeComments(bladeOverrideComments: boolean, onStart: boolean = false): vscode.CharacterPair | void { // Is enabled AND blade langId is NOT set as disabled... if (bladeOverrideComments === true && !this.isLangIdDisabled("blade")) { + const bladeComments: vscode.CharacterPair = ["{{--", "--}}"]; + if (onStart) { - return ["{{--", "--}}"]; + return bladeComments; } else { vscode.languages.setLanguageConfiguration("blade", { comments: { - blockComment: ["{{--", "--}}"], + blockComment: bladeComments, }, }); } } // Is disabled OR blade langId is set as disabled... else if (!bladeOverrideComments || this.isLangIdDisabled("blade")) { + const htmlComments: vscode.CharacterPair = [""]; + if (onStart) { - return [""]; + return htmlComments; } else { vscode.languages.setLanguageConfiguration("blade", { comments: { - blockComment: [""], + blockComment: htmlComments, }, }); } @@ -212,24 +229,22 @@ export class Configuration { * @returns {vscode.WorkspaceConfiguration} */ public getConfiguration(): vscode.WorkspaceConfiguration { - return vscode.workspace.getConfiguration(this.extensionData.get("name"), null); + return vscode.workspace.getConfiguration(this.extensionData.get("namespace"), null); } /** * Get value of the specified key from the extension's user configuration settings. * - * @param {string} key The key of the specific setting. - * - * @returns {T} Returns the value of the `key`. + * @param {K} key The key of the specific setting. * - * NOTE: Return is typed as `T`, which is a generic type that represents the type that is declared when called (as explained in this StackOverflow answer: https://stackoverflow.com/a/49622066/2358222) + * @returns {Settings[K]} Returns the value of the `key` with proper typing. * * @example ```ts - * this.getConfigurationValue("disabledLanguages"); + * this.getConfigurationValue("disabledLanguages"); // Returns string[] with full type safety * ``` */ - public getConfigurationValue(key: string): T { - return this.getConfiguration().get(key); + public getConfigurationValue(key: K): Settings[K] { + return this.getConfiguration().get(key); } /** @@ -237,34 +252,34 @@ export class Configuration { * * @param {string} key The key of the specific setting. - * @param {any} value The value to update the setting with. + * @param {unknown} value The value to update the setting with. * * @example ```ts * this.updateConfigurationValue("bladeOverrideComments", true); * ``` */ - public updateConfigurationValue(key: string, value: any) { + public updateConfigurationValue(key: string, value: unknown) { // .update(config key, new value, global) this.getConfiguration().update(key, value, true); } /** * Is the language ID disabled? - * @param {string} langId Language ID + * @param {LanguageId} langId Language ID * @returns {boolean} */ - public isLangIdDisabled(langId: string): boolean { - return this.getConfigurationValue("disabledLanguages").includes(langId); + public isLangIdDisabled(langId: LanguageId): boolean { + return this.getConfigurationValue("disabledLanguages").includes(langId); } /** * Is the multi-line comment overridden for the specified language ID? * - * @param {string} langId Language ID + * @param {LanguageId} langId Language ID * @returns {boolean} */ - private isLangIdMultiLineCommentOverridden(langId: string): boolean { - const overriddenList = this.getConfigurationValue("overrideDefaultLanguageMultiLineComments"); + private isLangIdMultiLineCommentOverridden(langId: LanguageId): boolean { + const overriddenList = this.getConfigurationValue("overrideDefaultLanguageMultiLineComments"); return overriddenList.hasOwnProperty(langId); } @@ -272,11 +287,11 @@ export class Configuration { /** * Get the overridden multi-line comment for the specified language ID. * - * @param {string} langId Language ID - * @returns {string} + * @param {LanguageId} langId Language ID + * @returns {string} The overridden multi-line comment style. */ - private getOverriddenMultiLineComment(langId: string) { - const overriddenList = this.getConfigurationValue("overrideDefaultLanguageMultiLineComments"); + private getOverriddenMultiLineComment(langId: LanguageId): string { + const overriddenList = this.getConfigurationValue("overrideDefaultLanguageMultiLineComments"); return overriddenList[langId]; } @@ -286,11 +301,11 @@ export class Configuration { * * Idea from this StackOverflow answer https://stackoverflow.com/a/72988011/2358222 * - * @returns {string[]} + * @returns {JsonArray} */ - private getLanguagesToSkip(): string[] { + private getLanguagesToSkip(): JsonArray { const json = utils.readJsonFile(`${__dirname}/../../config/skip-languages.jsonc`); - return json.languages; + return json.languages as JsonArray; } /** @@ -298,13 +313,13 @@ export class Configuration { * (built-in and 3rd party). */ private findAllLanguageConfigFilePaths() { - const extensions: any[] = []; + const extensions: ExtensionMetaData[] = []; // If running in WSL... if (isWsl) { // Get the Windows user and built-in extensions paths. - const windowsUserExtensionsPath = this.extensionData.get("WindowsUserExtensionsPathFromWsl"); - const windowsBuiltInExtensionsPath = this.extensionData.get("WindowsBuiltInExtensionsPathFromWsl"); + const windowsUserExtensionsPath = this.extensionData.getExtensionDiscoveryPath("WindowsUserExtensionsPathFromWsl"); + const windowsBuiltInExtensionsPath = this.extensionData.getExtensionDiscoveryPath("WindowsBuiltInExtensionsPathFromWsl"); // Read the paths and create arrays of the extensions. const windowsBuiltInExtensions = this.readExtensionsFromDirectory(windowsBuiltInExtensionsPath); @@ -314,8 +329,8 @@ export class Configuration { extensions.push(...windowsBuiltInExtensions, ...windowsUserExtensions); } - const userExtensionsPath = this.extensionData.get("userExtensionsPath"); - const builtInExtensionsPath = this.extensionData.get("builtInExtensionsPath"); + const userExtensionsPath = this.extensionData.getExtensionDiscoveryPath("userExtensionsPath"); + const builtInExtensionsPath = this.extensionData.getExtensionDiscoveryPath("builtInExtensionsPath"); // Read the paths and create arrays of the extensions. const userExtensions = this.readExtensionsFromDirectory(userExtensionsPath); @@ -334,7 +349,7 @@ export class Configuration { if (Object.hasOwn(packageJSON, "contributes") && Object.hasOwn(packageJSON.contributes, "languages")) { // Loop through the languages... for (let language of packageJSON.contributes.languages) { - const langId = language.id; + const langId: LanguageId = language.id; // Get the languages to skip. let skipLangs = this.getLanguagesToSkip(); @@ -371,7 +386,7 @@ export class Configuration { this.languageConfigFilePaths.forEach((paths, langId) => { // Loop through the paths array... paths.forEach((filepath) => { - let config = utils.readJsonFile(filepath); + let config = utils.readJsonFile(filepath) as vscode.LanguageConfiguration; // If the config JSON has more than 0 keys (ie. not empty) if (Object.keys(config).length > 0) { @@ -451,10 +466,10 @@ export class Configuration { /** * Get the config of the specified language. * - * @param langId Language ID + * @param {LanguageId} langId Language ID * @returns {vscode.LanguageConfiguration | undefined} */ - private getLanguageConfig(langId: string) { + private getLanguageConfig(langId: LanguageId): vscode.LanguageConfiguration | undefined { if (this.languageConfigs.has(langId)) { return this.languageConfigs.get(langId); } @@ -470,12 +485,11 @@ export class Configuration { * * @param {string} extensionsPath The path where extensions are stored. * - * @returns {Array<{ id: string; extensionPath: string; packageJSON: IPackageJson }>} + * @returns {ExtensionMetaData[]} */ - private readExtensionsFromDirectory(extensionsPath: string): Array<{id: string; extensionPath: string; packageJSON: IPackageJson}> { + private readExtensionsFromDirectory(extensionsPath: string): ExtensionMetaData[] { // Create an array to hold the found extensions. - const foundExtensions: Array<{id: string; extensionPath: string; packageJSON: IPackageJson}> = []; - + const foundExtensions: ExtensionMetaData[] = []; fs.readdirSync(extensionsPath).forEach((extensionName) => { const extensionPath = path.join(extensionsPath, extensionName); @@ -486,18 +500,14 @@ export class Configuration { return; } - // Get the package.json file path. - const packageJSONPath = path.join(extensionPath, "package.json"); - - // If the package.json file exists... - if (fs.existsSync(packageJSONPath)) { - const packageJSON: IPackageJson = utils.readJsonFile(packageJSONPath); + const extensionData = new ExtensionData(extensionPath).getAll(); - const id = `${packageJSON.publisher}.${packageJSON.name}`; - - // Push the extension data object into the array. - foundExtensions.push({id, extensionPath, packageJSON}); + if (extensionData === null) { + return; } + + // Push the extension data object into the array. + foundExtensions.push(extensionData); } }); @@ -508,29 +518,31 @@ export class Configuration { * Get the multi-line languages from the Map. * * @param {"supportedLanguages" | "customSupportedLanguages"} key A stringed key, either `"supportedLanguages"` or `"customSupportedLanguages"` - * @returns {string[]} An array of language ID strings. + * @returns {LanguageId[]} An array of language ID strings. */ - private getMultiLineLanguages(key: "supportedLanguages" | "customSupportedLanguages"): string[] { - return this.multiLineBlocksMap.get(key); + private getMultiLineLanguages(key: "supportedLanguages" | "customSupportedLanguages"): LanguageId[] { + // The non-null assertion operator (!) ensures that the key is never undefined. + return this.multiLineBlocksMap.get(key)!; } /** * Get the single-line languages and styles. * * @param {"supportedLanguages" | "customSupportedLanguages"} key A stringed key, either `"supportedLanguages"` or `"customSupportedLanguages"` - * @returns {Map} The Map of the languages and styles. + * @returns {Map} The Map of the languages and styles. */ - private getSingleLineLanguages(key: "supportedLanguages" | "customSupportedLanguages"): Map { - return this.singleLineBlocksMap.get(key); + private getSingleLineLanguages(key: "supportedLanguages" | "customSupportedLanguages"): Map { + // The non-null assertion operator (!) ensures that the key is never undefined. + return this.singleLineBlocksMap.get(key)!; } /** * Set the multi-line comments language definitions. */ private setMultiLineCommentLanguageDefinitions() { - let langArray = []; + let langArray: LanguageId[] = []; - this.languageConfigs.forEach((config: any, langId: string) => { + this.languageConfigs.forEach((config: vscode.LanguageConfiguration, langId: LanguageId) => { // If the config object has own property of comments AND the comments key has // own property of blockComment... if (Object.hasOwn(config, "comments") && Object.hasOwn(config.comments, "blockComment")) { @@ -552,7 +564,7 @@ export class Configuration { // for sanity reasons. this.multiLineBlocksMap.set("supportedLanguages", langArray.sort()); - const multiLineStyleBlocksLangs = this.getConfigurationValue("multiLineStyleBlocks"); + const multiLineStyleBlocksLangs = this.getConfigurationValue("multiLineStyleBlocks"); // Empty the langArray to reuse it. langArray = []; @@ -574,20 +586,20 @@ export class Configuration { * Set the single-line comments language definitions. */ private setSingleLineCommentLanguageDefinitions() { - let style: string; - const tempMap: Map = new Map(); - this.languageConfigs.forEach((config: any, langId: string) => { + const tempMap: Map = new Map(); + + this.languageConfigs.forEach((config: vscode.LanguageConfiguration, langId: LanguageId) => { // console.log(langId, config.comments.lineComment); - let style: string = ""; + let style: SingleLineCommentStyle | null = null; // If the config object has own property of comments AND the comments key has // own property of lineComment... if (Object.hasOwn(config, "comments") && Object.hasOwn(config.comments, "lineComment")) { - let lineComment = config.comments.lineComment; + let lineComment: LineComment = config.comments.lineComment as LineComment; // Line comments can be a string or an object with a "comment" key. // If the lineComment is an object, get the "comment" key value. - if (Object.hasOwn(lineComment, "comment")) { + if (typeof lineComment === "object" && lineComment !== null && Object.hasOwn(lineComment, "comment")) { lineComment = lineComment.comment; } @@ -599,15 +611,15 @@ export class Configuration { else if (lineComment === "#") { style = "#"; } - // If the lineComment includes a ";" (; or ;;)... - else if (lineComment.includes(";")) { + // If the lineComment is ";" or ";;"... + else if (lineComment === ";" || lineComment === ";;") { style = ";"; } - // If style is NOT an empty string, (i.e. not an unsupported single-line + // If style is NOT null, (i.e. not an unsupported single-line // comment like bat's @rem), AND // the langId isn't set as disabled... - if (style != "" && !this.isLangIdDisabled(langId)) { + if (style !== null && !this.isLangIdDisabled(langId)) { // Set the langId and it's style into the Map. tempMap.set(langId, style); } @@ -622,7 +634,7 @@ export class Configuration { tempMap.clear(); // Get user-customized langIds for the //-style and add to the map. - let customSlashLangs = this.getConfigurationValue("slashStyleBlocks"); + let customSlashLangs = this.getConfigurationValue("slashStyleBlocks"); for (let langId of customSlashLangs) { // If langId is exists (ie. not NULL or empty string) AND // the langId is longer than 0, AND @@ -633,7 +645,7 @@ export class Configuration { } // Get user-customized langIds for the #-style and add to the map. - let customHashLangs = this.getConfigurationValue("hashStyleBlocks"); + let customHashLangs = this.getConfigurationValue("hashStyleBlocks"); for (let langId of customHashLangs) { // If langId is exists (ie. not NULL or empty string) AND // the langId is longer than 0, AND @@ -644,7 +656,7 @@ export class Configuration { } // Get user-customized langIds for the ;-style and add to the map. - let customSemicolonLangs = this.getConfigurationValue("semicolonStyleBlocks"); + let customSemicolonLangs = this.getConfigurationValue("semicolonStyleBlocks"); for (let langId of customSemicolonLangs) { // If langId is exists (ie. not NULL or empty string) AND // the langId is longer than 0, AND @@ -667,18 +679,24 @@ export class Configuration { // Ensure the auto-generated directory exists. utils.ensureDirExists(this.autoGeneratedDir); - // Write the into the single-line-languages.json file. - utils.writeJsonFile(this.singleLineLangDefinitionFilePath, utils.convertMapToReversedObject(this.singleLineBlocksMap)); - // Write the into the multi-line-languages.json file. - utils.writeJsonFile(this.multiLineLangDefinitionFilePath, Object.fromEntries(this.multiLineBlocksMap)); + // Convert the singleLineBlocksMap to an object. + const singleLineData = utils.convertMapToReversedObject(this.singleLineBlocksMap); + + const multiLineData = Object.fromEntries(this.multiLineBlocksMap) as unknown as MultiLineLanguageDefinitions; + + // Write into the single-line-languages.json file. + utils.writeJsonFile(this.singleLineLangDefinitionFilePath, singleLineData); + + // Write into the multi-line-languages.json file. + utils.writeJsonFile(this.multiLineLangDefinitionFilePath, multiLineData); } /** * Sets the language configuration for a given language ID. * - * @param {string} langId - The language ID for which the configuration is being set. - * @param {boolean} multiLine - Optional. If true, sets multi-line comment configuration. - * @param {string} singleLineStyle - Optional. Specifies the style of single-line comments (e.g., "//", "#", ";"). + * @param {LanguageId} langId - The language ID for which the configuration is being set. + * @param {boolean} multiLine - Optional. If `true`, sets multi-line comment configuration. + * @param {SingleLineCommentStyle} singleLineStyle - Optional. Specifies the style of single-line comments (e.g., `"//"`, `"#"`, `";"`). * * @returns {vscode.Disposable} * @@ -695,17 +713,25 @@ export class Configuration { * Note: This method ensures that the language configuration is correctly set and avoids issues * with rogue characters being inserted on new lines. */ - private setLanguageConfiguration(langId: string, multiLine?: boolean, singleLineStyle?: string): vscode.Disposable { + private setLanguageConfiguration(langId: LanguageId, multiLine?: boolean, singleLineStyle?: SingleLineCommentStyle): vscode.Disposable { const internalLangConfig: vscode.LanguageConfiguration = this.getLanguageConfig(langId); - const defaultMultiLineConfig: any = utils.readJsonFile(`${__dirname}/../../config/default-multi-line-config.json`); + const defaultMultiLineConfig = utils.readJsonFile(`${__dirname}/../../config/default-multi-line-config.json`) as vscode.LanguageConfiguration; let langConfig = {...internalLangConfig}; if (multiLine) { - langConfig.autoClosingPairs = utils.mergeArraysBy(defaultMultiLineConfig.autoClosingPairs, internalLangConfig?.autoClosingPairs, "open"); + langConfig.autoClosingPairs = utils.mergeArraysBy( + defaultMultiLineConfig.autoClosingPairs, + internalLangConfig?.autoClosingPairs, + "open" + ); // Add the multi-line onEnter rules to the langConfig. - langConfig.onEnterRules = utils.mergeArraysBy(Rules.multilineEnterRules, internalLangConfig?.onEnterRules, "beforeText"); + langConfig.onEnterRules = utils.mergeArraysBy( + Rules.multilineEnterRules, + internalLangConfig?.onEnterRules, + "beforeText" + ); // Only assign the default config comments if it doesn't already exist. // (nullish assignment operator ??=) @@ -721,12 +747,16 @@ export class Configuration { * Get the user settings/configuration and set the blade or html comments accordingly. */ if (langId === "blade") { - langConfig.comments.blockComment = this.setBladeComments(this.getConfigurationValue("bladeOverrideComments"), true); + const bladeComments = this.setBladeComments(this.getConfigurationValue("bladeOverrideComments"), true); + + // If bladeComments is has a value... + if (bladeComments) { + langConfig.comments.blockComment = bladeComments; + } } } - let isOnEnter = this.getConfigurationValue("singleLineBlockOnEnter"); - + let isOnEnter = this.getConfigurationValue("singleLineBlockOnEnter"); // Add the single-line onEnter rules to the langConfig. // // If isOnEnter is true AND singleLineStyle isn't false, i.e. is a string, @@ -734,15 +764,15 @@ export class Configuration { if (isOnEnter && singleLineStyle) { // //-style comments if (singleLineStyle === "//") { - langConfig.onEnterRules = utils.mergeArraysBy(Rules.slashEnterRules, langConfig?.onEnterRules, "beforeText"); + langConfig.onEnterRules = utils.mergeArraysBy(Rules.slashEnterRules, langConfig?.onEnterRules, "beforeText"); } // #-style comments else if (singleLineStyle === "#") { - langConfig.onEnterRules = utils.mergeArraysBy(Rules.hashEnterRules, langConfig?.onEnterRules, "beforeText"); + langConfig.onEnterRules = utils.mergeArraysBy(Rules.hashEnterRules, langConfig?.onEnterRules, "beforeText"); } // ;-style comments else if (singleLineStyle === ";") { - langConfig.onEnterRules = utils.mergeArraysBy(Rules.semicolonEnterRules, langConfig?.onEnterRules, "beforeText"); + langConfig.onEnterRules = utils.mergeArraysBy(Rules.semicolonEnterRules, langConfig?.onEnterRules, "beforeText"); } } // If isOnEnter is false AND singleLineStyle isn't false, i.e. a string. @@ -864,13 +894,13 @@ export class Configuration { * @param {vscode.TextEditorEdit} edit The text editor edits. */ private handleSingleLineBlock(textEditor: vscode.TextEditor, edit: vscode.TextEditorEdit) { - let langId = textEditor.document.languageId; + let langId: LanguageId = textEditor.document.languageId; const singleLineLangs = this.getSingleLineLanguages("supportedLanguages"); const customSingleLineLangs = this.getSingleLineLanguages("customSupportedLanguages"); // Get the langId from the auto-supported langs. If it doesn't exist, try getting it from // the custom-supported langs instead. - var style = singleLineLangs.get(langId) ?? customSingleLineLangs.get(langId); + var style: SingleLineCommentStyle | ExtraSingleLineCommentStyles = singleLineLangs.get(langId) ?? customSingleLineLangs.get(langId); if (style && textEditor.selection.isEmpty) { let line = textEditor.document.lineAt(textEditor.selection.active); @@ -906,7 +936,7 @@ export class Configuration { } var indentedNewLine = "\n" + line.text.substring(0, line.text.search(indentRegex)); - let isOnEnter = this.getConfigurationValue("singleLineBlockOnEnter"); + let isOnEnter = this.getConfigurationValue("singleLineBlockOnEnter"); if (!isOnEnter) { indentedNewLine += style + " "; } @@ -922,13 +952,13 @@ export class Configuration { * @param {vscode.TextEditor} textEditor The text editor. */ private handleChangeBladeMultiLineBlock(textEditor: vscode.TextEditor) { - let langId = textEditor.document.languageId; - const extensionName = this.extensionData.get("name"); + let langId: LanguageId = textEditor.document.languageId; + const extensionName = this.extensionData.get("namespace"); // Only carry out function if languageId is blade. if (langId === "blade" && !this.isLangIdDisabled(langId)) { // Read current value - let isOverridden = this.getConfigurationValue("bladeOverrideComments"); + let isOverridden = this.getConfigurationValue("bladeOverrideComments"); if (isOverridden === false) { // Update to true @@ -938,7 +968,7 @@ export class Configuration { this.updateConfigurationValue("bladeOverrideComments", false); } // Read new value - let bladeOverrideComments = this.getConfigurationValue("bladeOverrideComments"); + let bladeOverrideComments = this.getConfigurationValue("bladeOverrideComments"); // Set the comments for blade language. this.setBladeComments(bladeOverrideComments); @@ -962,29 +992,29 @@ export class Configuration { private logDebugInfo() { // The path to the built-in extensions. The env variable changes when on WSL. // So we can use it for both Windows and WSL. - const builtInExtensionsPath = this.extensionData.get("builtInExtensionsPath"); + const builtInExtensionsPath = this.extensionData.getExtensionDiscoveryPath("builtInExtensionsPath"); - let extensionsPaths = {}; + let extensionsPaths: JsonObject = {}; if (isWsl) { // Get the Windows user and built-in extensions paths. - const windowsUserExtensionsPath = this.extensionData.get("WindowsUserExtensionsPathFromWsl"); - const windowsBuiltInExtensionsPath = this.extensionData.get("WindowsBuiltInExtensionsPathFromWsl"); + const windowsUserExtensionsPath = this.extensionData.getExtensionDiscoveryPath("WindowsUserExtensionsPathFromWsl"); + const windowsBuiltInExtensionsPath = this.extensionData.getExtensionDiscoveryPath("WindowsBuiltInExtensionsPathFromWsl"); extensionsPaths = { "Windows-installed Built-in Extensions Path": windowsBuiltInExtensionsPath, "Windows-installed User Extensions Path": windowsUserExtensionsPath, "WSL-installed Built-in Extensions Path": builtInExtensionsPath, - "WSL-installed User Extensions Path": this.extensionData.get("userExtensionsPath"), + "WSL-installed User Extensions Path": this.extensionData.getExtensionDiscoveryPath("userExtensionsPath"), }; } else { extensionsPaths = { "Built-in Extensions Path": builtInExtensionsPath, - "User Extensions Path": this.extensionData.get("userExtensionsPath"), + "User Extensions Path": this.extensionData.getExtensionDiscoveryPath("userExtensionsPath"), }; } - const env = { + const env: JsonObject = { "OS": process.platform, "Platform": process.platform, "VS Code Details": { @@ -1003,8 +1033,14 @@ export class Configuration { // Log the objects for debugging purposes. logger.debug("The language config filepaths found are:", this.languageConfigFilePaths); logger.debug("The language configs found are:", this.languageConfigs); - logger.debug("The supported languages for multi-line blocks:", utils.readJsonFile(this.multiLineLangDefinitionFilePath)); - logger.debug("The supported languages for single-line blocks:", utils.readJsonFile(this.singleLineLangDefinitionFilePath)); + logger.debug( + "The supported languages for multi-line blocks:", + utils.readJsonFile(this.multiLineLangDefinitionFilePath) + ); + logger.debug( + "The supported languages for single-line blocks:", + utils.readJsonFile(this.singleLineLangDefinitionFilePath) + ); } /** @@ -1022,6 +1058,7 @@ export class Configuration { rules.forEach((rule) => { if (rule.action && "indent" in rule.action) { // Convert JSON format to API format + // Usage of the `any` type is necessary here because `rule.action` const indentValue = (rule.action as any).indent; delete (rule.action as any).indent; diff --git a/src/extension.ts b/src/extension.ts index 4d24398..700dc39 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,11 +18,11 @@ export function activate(context: vscode.ExtensionContext) { disposables.push(...configureCommentBlocksDisposable, ...registerCommandsDisposable); - const extensionName = extensionData.get("name"); + const extensionName = extensionData.get("namespace"); const extensionDisplayName = extensionData.get("displayName"); - let disabledLangConfig: string[] = configuration.getConfigurationValue("disabledLanguages"); + let disabledLangConfig: string[] = configuration.getConfigurationValue("disabledLanguages"); if (disabledLangConfig.length > 0) { vscode.window.showInformationMessage(`${disabledLangConfig.join(", ")} languages are disabled for ${extensionDisplayName}.`); @@ -32,7 +32,7 @@ export function activate(context: vscode.ExtensionContext) { * When the configuration/user settings are changed, set the extension * to reflect the settings and output a message to the user. */ - vscode.workspace.onDidChangeConfiguration((event: any) => { + vscode.workspace.onDidChangeConfiguration((event: vscode.ConfigurationChangeEvent) => { // TODO: Work on automatically updating the languages instead of making the user reload the extension. /** @@ -41,7 +41,7 @@ export function activate(context: vscode.ExtensionContext) { // If the affected setting is bladeOverrideComments... if (event.affectsConfiguration(`${extensionName}.bladeOverrideComments`)) { // Get the setting. - let bladeOverrideComments: boolean = configuration.getConfigurationValue("bladeOverrideComments"); + let bladeOverrideComments: boolean = configuration.getConfigurationValue("bladeOverrideComments"); configuration.setBladeComments(bladeOverrideComments); diff --git a/src/extensionData.ts b/src/extensionData.ts index 473513a..c9db9c4 100644 --- a/src/extensionData.ts +++ b/src/extensionData.ts @@ -4,14 +4,29 @@ import isWsl from "is-wsl"; import {IPackageJson} from "package-json-type"; import {readJsonFile} from "./utils"; +import {ExtensionMetaData, ExtensionPaths, ExtensionMetaDataValue} from "./interfaces/extensionMetaData"; export class ExtensionData { /** - * This extension details in the form of a key:value Map object. + * Extension data in the form of a key:value Map object. * - * @type {Map} + * @type {Map} */ - private extensionData = new Map(); + private extensionData = new Map(); + + /** + * Extension discovery paths in the form of a key:value Map object. + * + * @type {Map} + */ + private extensionDiscoveryPaths = new Map(); + + /** + * The absolute path of the requested extension. + * + * @type {string} + */ + private readonly extensionPath: string; /** * The package.json data for this extension. @@ -20,74 +35,81 @@ export class ExtensionData { */ private packageJsonData: IPackageJson; - public constructor() { + public constructor(extensionPath: string | null = null) { + // Set the path if provided, otherwise default to this extension's path. + // + // For this extension's path, we use `__dirname` and go up two levels + // (from "out/src" to the extension root). This path is also used to locate all other + // user-installed extensions later for the `userExtensionsPath` discovery path. + this.extensionPath = extensionPath ?? path.join(__dirname, "../../"); + this.packageJsonData = this.getExtensionPackageJsonData(); - this.setExtensionData(); + + // Only proceed with extension data setup if packageJsonData is NOT null. + if (this.packageJsonData !== null) { + this.setExtensionData(); + } + + this.setExtensionDiscoveryPaths(); } /** * Get the names, id, and version of this extension from package.json. * - * @returns {IPackageJson} The package.json data for this extension, with extra custom keys. + * @returns {IPackageJson | null} The package.json data for this extension, with extra custom keys. */ - private getExtensionPackageJsonData(): IPackageJson { - const extensionPath = path.join(__dirname, "../../"); - - const packageJSON: IPackageJson = readJsonFile(path.join(extensionPath, "package.json")); - - // Set the id (publisher.name) into the packageJSON object as a new `id` key. - packageJSON.id = `${packageJSON.publisher}.${packageJSON.name}`; - packageJSON.extensionPath = extensionPath; - - // The configuration settings namespace is a shortened version of the extension name. - // We just need to replace "automatic" with "auto" in the name. - const settingsNamespace: string = packageJSON.name.replace("automatic", "auto"); - // Set the namespace to the packageJSON `configuration` object as a new `namespace` key. - packageJSON.contributes.configuration.namespace = settingsNamespace; - - return packageJSON; + private getExtensionPackageJsonData(): IPackageJson | null { + // Get the package.json file path. + const packageJSONPath = path.join(this.extensionPath, "package.json"); + return readJsonFile(packageJSONPath, false); } /** * Set the extension data into the extensionData Map. */ private setExtensionData() { - // Set all entries in the extensionData Map. - Object.entries(this.createExtensionData()).forEach(([key, value]) => { - this.extensionData.set(key, value); - }); + // Create the extension ID (publisher.name). + const id = `${this.packageJsonData.publisher}.${this.packageJsonData.name}`; + + // Set each key-value pair directly into the Map + this.extensionData.set("id", id); + this.extensionData.set("name", this.packageJsonData.name); + + // Only set the namespace if it dealing with this extension. + if (this.packageJsonData.name === "automatic-comment-blocks") { + // The configuration settings namespace is a shortened version of the extension name. + // We just need to replace "automatic" with "auto" in the name. + const settingsNamespace: string = this.packageJsonData.name.replace("automatic", "auto"); + + this.extensionData.set("namespace", settingsNamespace); + } + + this.extensionData.set("displayName", this.packageJsonData.displayName); + this.extensionData.set("version", this.packageJsonData.version); + this.extensionData.set("extensionPath", this.extensionPath); + this.extensionData.set("packageJSON", this.packageJsonData); } /** - * Create the extension data object for the extensionData Map. - * It also helps for type inference intellisense in the get method. - * - * @returns The extension data object with keys and values. + * Set the extension discovery paths into the extensionDiscoveryPaths Map. */ - private createExtensionData() { + private setExtensionDiscoveryPaths() { // The path to the user extensions. - const userExtensionsPath = isWsl - ? path.join(vscode.env.appRoot, "../../", "extensions") - : path.join(this.packageJsonData.extensionPath, "../"); - - // Set the keys and values for the Map. - // The keys will also be used for type inference in VSCode intellisense. - return { - id: this.packageJsonData.id, - name: this.packageJsonData.contributes.configuration.namespace, - displayName: this.packageJsonData.displayName, - version: this.packageJsonData.version, - userExtensionsPath: userExtensionsPath, - // The path to the built-in extensions. - // This env variable changes when on WSL to it's WSL-built-in extensions path. - builtInExtensionsPath: path.join(vscode.env.appRoot, "extensions"), - - // Only set these if running in WSL. - ...(isWsl && { - WindowsUserExtensionsPathFromWsl: path.dirname(process.env.VSCODE_WSL_EXT_LOCATION!), - WindowsBuiltInExtensionsPathFromWsl: path.join(process.env.VSCODE_CWD!, "resources/app/extensions"), - }), - } as const; + // + // On Windows/Linux/Mac: ~/.vscode[-server|remote]/extensions + // On WSL: ~/.vscode-[server|remote]/extensions + const userExtensionsPath = isWsl ? path.join(vscode.env.appRoot, "../../", "extensions") : path.join(this.extensionPath, "../"); + + this.extensionDiscoveryPaths.set("userExtensionsPath", userExtensionsPath); + // The path to the built-in extensions. + // This env variable changes when on WSL to it's WSL-built-in extensions path. + this.extensionDiscoveryPaths.set("builtInExtensionsPath", path.join(vscode.env.appRoot, "extensions")); + + // Only set these if running in WSL + if (isWsl) { + this.extensionDiscoveryPaths.set("WindowsUserExtensionsPathFromWsl", path.dirname(process.env.VSCODE_WSL_EXT_LOCATION!)); + this.extensionDiscoveryPaths.set("WindowsBuiltInExtensionsPathFromWsl", path.join(process.env.VSCODE_CWD!, "resources/app/extensions")); + } } /** @@ -95,18 +117,43 @@ export class ExtensionData { * * @param {K} key The key of the extension detail to get. * - * @returns {ReturnType[K] | undefined} The value of the extension detail, or undefined if the key does not exist. + * @returns {ExtensionMetaData[K] | undefined} The value of the extension detail, or undefined if the key does not exist. + */ + public get(key: K): ExtensionMetaData[K] | undefined { + return this.extensionData.get(key) as ExtensionMetaData[K] | undefined; + } + + /** + * Get all extension data as a plain object. + * + * @returns {ExtensionMetaData} A plain object containing all extension details. + */ + public getAll(): ExtensionMetaData | null { + // If no data, return null + if (this.extensionData.size === 0) { + return null; + } + + return Object.fromEntries(this.extensionData) as unknown as ExtensionMetaData; + } + + /** + * Get the extension discovery paths by a specified key. + * + * @param {K} key The key of the specific path to get. + * + * @returns {ExtensionPaths[K] | undefined} The value of the extension detail, or undefined if the key does not exist. */ - public get>(key: K): ReturnType[K] | undefined { - return this.extensionData.get(key) as ReturnType[K] | undefined; + public getExtensionDiscoveryPath(key: K): ExtensionPaths[K] | undefined { + return this.extensionDiscoveryPaths.get(key) as ExtensionPaths[K] | undefined; } /** - * Get all extension data. + * Get all extension discovery paths. * - * @returns {ReadonlyMap} A read-only Map containing all extension details. + * @returns {ReadonlyMap} A read-only Map containing all extension discovery paths. */ - public getAll(): ReadonlyMap { - return this.extensionData; + public getAllExtensionDiscoveryPaths(): ReadonlyMap { + return this.extensionDiscoveryPaths; } } diff --git a/src/interfaces/commentStyles.ts b/src/interfaces/commentStyles.ts new file mode 100644 index 0000000..0206fb2 --- /dev/null +++ b/src/interfaces/commentStyles.ts @@ -0,0 +1,41 @@ +/** + * Define the single-line comment styles. + */ +export type SingleLineCommentStyle = "//" | "#" | ";"; + +/** + * Define the extra single-line comment styles, like `///`, etc. + */ +export type ExtraSingleLineCommentStyles = "##" | ";;" | "///" | "//!"; + +/** + * Line Comments + * + * Taken directly from VScode's commit in June 2025 that changed the line comment config. + * https://github.com/microsoft/vscode/commit/d9145a291dcef0bad3ace81a3d55727ca294c122#diff-0dfa7db579eface8250affb76bc88717725a121401d4d8598bc36b92b0b6ef62 + * + * The @types/vscode package does not yet have these changes. + * So until they're added, we define them manually. + */ + +/** + * The line comment token, like `// this is a comment`. + * Can be a string, an object with comment and optional noIndent properties, or null. + */ +export type LineComment = string | LineCommentConfig | null; + +/** + * Configuration for line comments. + */ +export interface LineCommentConfig { + /** + * The line comment token, like `//` + */ + comment: string; + + /** + * Whether the comment token should not be indented and placed at the first column. + * Defaults to false. + */ + noIndent?: boolean; +} diff --git a/src/interfaces/extensionMetaData.ts b/src/interfaces/extensionMetaData.ts new file mode 100644 index 0000000..646ec81 --- /dev/null +++ b/src/interfaces/extensionMetaData.ts @@ -0,0 +1,77 @@ +import {IPackageJson} from "package-json-type"; + +// Utility types for cleaner Map typing +export type ExtensionMetaDataValue = ExtensionMetaData[keyof ExtensionMetaData]; + +/** + * Extension metadata for a VSCode extension + */ +export interface ExtensionMetaData { + /** + * The unique ID in the form of `publisher.name`. + */ + id: string; + + /** + * The name. + * Directly from package.json "name" key. + */ + name: string; + + /** + * The namespace for this extension's configuration settings, + * which is a slightly shorter version of the name. + */ + namespace?: string; + + /** + * The display name. + * Directly from package.json "displayName" key. + */ + displayName: string; + + /** + * The version. + * Directly from package.json "version" key. + */ + version: string; + + /** + * The absolute path to the extension. + */ + extensionPath: string; + + /** + * The full package.json data + */ + packageJSON: IPackageJson; +} + +/** + * Extension discovery paths configuration for this extension + */ +export interface ExtensionPaths { + /** + * The path to the user extensions. + */ + userExtensionsPath: string; + + /** + * The path to the built-in extensions. + */ + builtInExtensionsPath: string; + + /** + * The Windows path to the user extensions when running in WSL. + * + * Only set when running in WSL. + */ + WindowsUserExtensionsPathFromWsl?: string; + + /** + * The Windows path to the built-in extensions when running in WSL. + * + * Only set when running in WSL. + */ + WindowsBuiltInExtensionsPathFromWsl?: string; +} diff --git a/src/interfaces/settings.ts b/src/interfaces/settings.ts new file mode 100644 index 0000000..da080b7 --- /dev/null +++ b/src/interfaces/settings.ts @@ -0,0 +1,10 @@ +export interface Settings { + singleLineBlockOnEnter: boolean; + disabledLanguages: string[]; + slashStyleBlocks: string[]; + hashStyleBlocks: string[]; + semicolonStyleBlocks: string[]; + multiLineStyleBlocks: string[]; + overrideDefaultLanguageMultiLineComments: Record; + bladeOverrideComments: boolean; +} diff --git a/src/interfaces/utils.ts b/src/interfaces/utils.ts new file mode 100644 index 0000000..404880f --- /dev/null +++ b/src/interfaces/utils.ts @@ -0,0 +1,39 @@ +import {SingleLineCommentStyle} from "./commentStyles"; + +/** + * Represents a JSON object. + */ +export interface JsonObject { + [key: string]: JsonValue; +} + +/** + * Represents a valid JSON value. + */ +export type JsonValue = string | number | boolean | null | JsonObject | JsonArray; + +/** + * Represents a JSON array. + */ +export type JsonArray = JsonValue[] | readonly JsonValue[]; + +/** + * Structure for single-line language definitions JSON file + */ +export interface SingleLineLanguageDefinitions extends JsonObject { + supportedLanguages: Record; + customSupportedLanguages: Record; +} + +/** + * Structure for multi-line language definitions JSON file + */ +export interface MultiLineLanguageDefinitions extends JsonObject { + supportedLanguages: string[]; + customSupportedLanguages: string[]; +} + +/** + * Language ID + */ +export type LanguageId = string; diff --git a/src/logger.ts b/src/logger.ts index 3d9c58e..91ef9fb 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -33,7 +33,7 @@ class Logger { /** * Override the output channel * - * @param {OutputChannel} channel A vscode output channel. + * @param {OutputChannel} channelOverride A vscode output channel. */ public setupOutputChannel(channelOverride?: OutputChannel): void { if (channelOverride) { @@ -84,9 +84,9 @@ class Logger { * This is helpful for logging objects and arrays. * * @param {string} message The message to be logged. - * @param {unknown} data Extra data that is useful for debugging, like an object or array. + * @param {unknown} data [Optional] Extra data that is useful for debugging, like an object or array. */ - public debug(message: string, data: unknown): void { + public debug(message: string, data?: unknown): void { if (this.debugMode) { this.logMessage("DEBUG", message, data); } diff --git a/src/utils.ts b/src/utils.ts index ea4faba..dff78eb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,16 +2,33 @@ import * as fs from "node:fs"; import * as jsonc from "jsonc-parser"; import {logger} from "./logger"; import {window} from "vscode"; +import {JsonObject, JsonValue} from "./interfaces/utils"; /** * Read the file and parse the JSON. * * @param {string} filepath The path of the file. + * @param {boolean} [throwOnFileMissing=true] Whether to throw an error if the file doesn't exist. + * If `false`, returns `null`. Default is `true`. * - * @returns {any} The JSON file content as an object. - * @throws Will throw an error if the JSON file cannot be parsed. + * @returns {T | null} The JSON file content as the passed T type (defaulting to a JSON object) + * or `null` if file doesn't exist and `throwOnFileMissing` is `false`. + * @throws Will throw an error if the JSON file cannot be parsed or if file doesn't exist and `throwOnFileMissing` is `true`. */ -export function readJsonFile(filepath: string): any { +export function readJsonFile(filepath: string, throwOnFileMissing: boolean = true): T | null { + // Check if file exists first. + // If file doesn't exist... + if (!fs.existsSync(filepath)) { + // If throwOnFileMissing param is true, throw an error. + if (throwOnFileMissing) { + const error = new Error(`JSON file not found: "${filepath}"`); + logger.error(error.stack); + throw error; + } + // Otherwise just return null. + return null; + } + const jsonErrors: jsonc.ParseError[] = []; const fileContent = fs @@ -42,7 +59,7 @@ export function readJsonFile(filepath: string): any { throw error; } - return jsonContents; + return jsonContents as T; } /** @@ -79,10 +96,9 @@ function constructJsonParseErrorMsg(filepath: string, fileContent: string, jsonE * Read the file and parse the JSON. * * @param {string} filepath The path of the file. - * @param {any} data The data to write into the file. - * @returns The file content. + * @param {JsonValue} data The data to write into the file. */ -export function writeJsonFile(filepath: string, data: any): any { +export function writeJsonFile(filepath: string, data: JsonValue) { // Write the updated JSON back into the file and add tab indentation // to make it easier to read. fs.writeFileSync(filepath, JSON.stringify(data, null, "\t")); @@ -103,11 +119,11 @@ export function ensureDirExists(dir: string) { * Reconstruct the regex pattern because vscode doesn't like the regex pattern as a string, * or some patterns are not working as expected. * - * @param obj The object - * @param key The key to check in the object + * @param {unknown} obj The object + * @param {string} key The key to check in the object * @returns {RegExp} The reconstructed regex pattern. */ -export function reconstructRegex(obj: any, key: string) { +export function reconstructRegex(obj: unknown, key: string): RegExp { // If key has a "pattern" key, then it's an object... if (Object.hasOwn(obj[key], "pattern")) { return new RegExp(obj[key].pattern); @@ -124,7 +140,7 @@ export function reconstructRegex(obj: any, key: string) { * Code based on this StackOverflow answer https://stackoverflow.com/a/45728850/2358222 * * @param {Map>} m The Map to convert to an object. - * @returns {object} The converted object. + * @returns {T} The converted object. * * @example * reverseMapping( @@ -150,8 +166,8 @@ export function reconstructRegex(obj: any, key: string) { * } * } */ -export function convertMapToReversedObject(m: Map>): object { - const result: any = {}; +export function convertMapToReversedObject(m: Map>): T { + const result = {}; // Convert a nested key:value Map from inside another Map into an key:array object, // while reversing/switching the keys and values. The Map's values are now the keys of @@ -175,7 +191,7 @@ export function convertMapToReversedObject(m: Map>): // as the key. result[key] = Object.keys(o).reduce((r, k) => Object.assign(r, {[o[k]]: (r[o[k]] || []).concat(k)}), {}); } - return result; + return result as T; } /** @@ -204,8 +220,7 @@ export function mergeArraysBy(primaryArray: T[], secondaryArray: T[], key: ke // Start with primary array (avoids side effects) const merged = [...primary]; - // Add items from secondary array that don't exist in primary, - // removing any duplicates. + // Add items from secondary array that don't exist in primary, removing any duplicates. secondary.forEach((item) => { // Test all items in the merged array to check if the value of the key // already exists in the merged array.