diff --git a/changelog.md b/changelog.md index ae535c76..3a897cf1 100644 --- a/changelog.md +++ b/changelog.md @@ -2,8 +2,8 @@ ## 6.3.0 - unreleased 🕳️ -- Rename extension to `AHK++ (AutoHotkey Plus Plus)` to provide a clear short name while retaining previous brand - - In 6.2.0, only the settings were renamed. This release renames the extension display name on registries as well. +### New features + - Rewrite AutoHotkey v2 definition files using [GroggyOtter](https://github.com/GroggyOtter/ahkv2_definition_rewrite)'s syntaxes ([#521](https://github.com/mark-wiemer-org/ahkpp/issues/521)) - Add exclude setting ([#488](https://github.com/mark-wiemer-org/ahkpp/issues/488)) - Excluded files are not included in IntelliSense completion suggestions, even when they're added via `#include` @@ -12,8 +12,17 @@ - Changes to this setting take effect immediately, no need to restart your IDE (different than thqby's extension) - v2 will exclude excluded files from suggestions even if they're opened in the IDE (different than thqby's extension) - v1 no longer automatically ignores files with `out`, `target`, or `node_modules` in their name + +### Fixes + - Fixup output channel names: "AHK++ (v1)" and "AHK++ (v2)" instead of "AHK" and "AHK++" respectively - Fix duplicate output channels +- Fix formatter removing extra spaces in v1 strings ([#411](https://github.com/mark-wiemer-org/ahkpp/issues/411)) + +### Other + +- Rename extension to `AHK++ (AutoHotkey Plus Plus)` to provide a clear short name while retaining previous brand + - In 6.2.0, only the settings were renamed. This release renames the extension display name on registries as well. ## 6.2.3 - 2024-10-08 📖 diff --git a/demos/.gitignore b/demos/.gitignore new file mode 100644 index 00000000..6a088f68 --- /dev/null +++ b/demos/.gitignore @@ -0,0 +1 @@ +sandbox.ah* \ No newline at end of file diff --git a/package.json b/package.json index 94b32753..3b2fdc6d 100644 --- a/package.json +++ b/package.json @@ -342,8 +342,8 @@ }, "trimExtraSpaces": { "type": "boolean", - "default": true, - "description": "Trim extra spaces between words." + "default": false, + "description": "%ahk++.config.v1.formatter.trimExtraSpaces%" } }, "default": { @@ -351,7 +351,7 @@ "indentCodeAfterIfDirective": true, "indentCodeAfterLabel": true, "preserveIndent": false, - "trimExtraSpaces": true + "trimExtraSpaces": false }, "additionalProperties": { "type": "string" diff --git a/package.nls.json b/package.nls.json index eb910c98..73be3b1f 100644 --- a/package.nls.json +++ b/package.nls.json @@ -11,9 +11,11 @@ "ahk++.command.setV2Interpreter": "Set AHK v2 Interpreter", "ahk++.command.stop": "Stop AHK Script", "ahk++.command.updateVersionInfo": "Update File Version Info", + "ahk++.config.exclude": "[Glob patterns]() for excluding files and folders from completion suggestions. Applies even when files are opened.", "ahk++.config.general.showOutput": "Automatically show output view when running a script. View can always be toggled with F1 > 'View: Toggle Output' (`Ctrl+Shift+U`)", "ahk++.config.general.showOutput.always": "Always open the output view when running a script", "ahk++.config.general.showOutput.never": "Never automatically show the output view", + "ahk++.config.v1.formatter.trimExtraSpaces": "Trim extra spaces between words and symbols", "ahk++.config.v2.actionWhenV1Detected": "Action when v1 script is detected", "ahk++.config.v2.commentTagRegex": "The regular expression for custom symbols to appear in the breadcrumb and elsewhere. Default matches any line that starts with `;;`. Changes take effect after restart.", "ahk++.config.v2.completionCommitCharacters": "Characters which commit auto-completion", @@ -22,7 +24,6 @@ "ahk++.config.v2.diagnostics.classNonDynamicMemberCheck": "Check whether non-dynamic members of a class exist", "ahk++.config.v2.diagnostics.paramsCheck": "Check that the function call has the correct number of arguments", "ahk++.config.v2.file.interpreterPath": "Path to the `AutoHotkey.exe` executable file for AHK v2.", - "ahk++.config.exclude": "[Glob patterns]() for excluding files and folders from completion suggestions. Applies even when files are opened.", "ahk++.config.v2.file.maxScanDepth": "Depth of folders to scan for IntelliSense. Negative values mean infinite depth.", "ahk++.config.v2.librarySuggestions": "Which libraries to suggest functions from, if any. In case of issues, restart your IDE.", "ahk++.config.v2.symbolFoldingFromOpenBrace": "Fold parameter lists separately from definitions.", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index f14700db..d045a3ed 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -17,6 +17,7 @@ "ahk++.config.general.showOutput": "运行脚本时自动显示输出视图。可通过 F1 > '视图.切换输出' (`Ctrl+Shift+U`)随时切换视图: Toggle Output' (`Ctrl+Shift+U`)", "ahk++.config.general.showOutput.always": "运行脚本时始终打开输出视图", "ahk++.config.general.showOutput.never": "从不自动显示输出视图", + "ahk++.config.v1.formatter.trimExtraSpaces": "删除单词和符号之间多余的空格", "ahk++.config.v2.actionWhenV1Detected": "检测到v1脚本时的行为", "ahk++.config.v2.commentTagRegex": "用来从注释中提取命名标记的正则表达式, 并生成模块符号", "ahk++.config.v2.completeFunctionCalls": "当右侧不存在`(`或`[`时, 给函数补全添加括号; 否则向右移动光标", diff --git a/src/providers/formattingProvider.e2e.ts b/src/providers/formattingProvider.e2e.ts index 9d1866b9..d4ca2d8b 100644 --- a/src/providers/formattingProvider.e2e.ts +++ b/src/providers/formattingProvider.e2e.ts @@ -3,17 +3,19 @@ import * as assert from 'assert'; import * as fs from 'fs-extra'; import * as path from 'path'; import * as vscode from 'vscode'; -import { FormatProvider, internalFormat } from './formattingProvider'; +import { FormatProvider } from './formattingProvider'; import { FormatOptions } from './formattingProvider.types'; const inFilenameSuffix = '.in.ahk'; const outFilenameSuffix = '.out.ahk'; + interface FormatTest { /** Name of the file, excluding the suffix (@see inFilenameSuffix, @see outFilenameSuffix) */ filenameRoot: string; // Any properties not provided will use `defaultOptions` below options?: Partial; } + /** Default formatting options, meant to match default extension settings */ const defaultOptions = { tabSize: 4, @@ -24,87 +26,6 @@ const defaultOptions = { preserveIndent: false, trimExtraSpaces: true, }; -const formatTests: FormatTest[] = [ - { filenameRoot: '25-multiline-string' }, - { filenameRoot: '28-switch-case' }, - { filenameRoot: '40-command-inside-text' }, - { filenameRoot: '55-if-directive' }, - { filenameRoot: '56-return-command-after-label' }, - { filenameRoot: '58-parentheses-indentation' }, - { filenameRoot: '59-one-command-indentation' }, - { filenameRoot: '72-paren-hotkey' }, - { filenameRoot: '119-semicolon-inside-string' }, - { filenameRoot: '161-colon-on-last-position' }, - { filenameRoot: '180-if-else-braces' }, - { - filenameRoot: '182-multiple-newlines', - options: { allowedNumberOfEmptyLines: 2 }, - }, - { filenameRoot: '184-continuation-section-expression' }, - { filenameRoot: '184-continuation-section-object' }, - { filenameRoot: '184-continuation-section-text' }, - { filenameRoot: '185-block-comment' }, - { - filenameRoot: '187-comments-at-end-of-line', - options: { trimExtraSpaces: false }, - }, - { filenameRoot: '188-one-command-code-in-text' }, - { filenameRoot: '189-space-at-end-of-line' }, - { - filenameRoot: '192-preserve-indent-true', - options: { preserveIndent: true }, - }, - { filenameRoot: '255-close-brace' }, - { filenameRoot: '255-else-if' }, - { filenameRoot: '255-if-loop-mix' }, - { filenameRoot: '255-return-function' }, - { filenameRoot: '255-return-label' }, - { filenameRoot: '255-style-allman' }, - { filenameRoot: '255-style-k-and-r' }, - { filenameRoot: '255-style-mix' }, - { filenameRoot: '255-style-one-true-brace' }, - { filenameRoot: '290-ifmsgbox' }, - { filenameRoot: '291-single-line-comment' }, - { filenameRoot: '316-if-object-continuation-section' }, - { filenameRoot: '429-single-line-hotkey' }, - { filenameRoot: '432-label-inside-code-block' }, - { filenameRoot: '440-fall-through-single-line-hotkey-with-if-directive' }, - { filenameRoot: '442-fall-through-single-line-hotkey-with-function' }, - { filenameRoot: 'ahk-explorer' }, - { filenameRoot: 'align-assignment' }, - { filenameRoot: 'demo' }, - { - filenameRoot: 'indent-code-after-if-directive-false', - options: { indentCodeAfterIfDirective: false }, - }, - { - filenameRoot: 'indent-code-after-if-directive-true', - options: { indentCodeAfterIfDirective: true }, - }, - { - filenameRoot: 'indent-code-after-label-false', - options: { indentCodeAfterLabel: false }, - }, - { - filenameRoot: 'indent-code-after-label-true', - options: { indentCodeAfterLabel: true }, - }, - { - filenameRoot: 'insert-spaces-false', - options: { insertSpaces: false }, - }, - { filenameRoot: 'legacy-text-if-directive' }, - { filenameRoot: 'label-colon' }, - { filenameRoot: 'label-combination' }, - { filenameRoot: 'label-fall-through' }, - { filenameRoot: 'label-specific-name' }, - { filenameRoot: 'return-exit-exitapp' }, - { filenameRoot: 'single-line-comment' }, - { - filenameRoot: 'tab-size-2', - options: { tabSize: 2 }, - }, -]; // Currently in `out` folder, need to get back to main `src` folder const filesParentPath = path.join( @@ -117,33 +38,6 @@ const filesParentPath = path.join( 'samples', // ./src/providers/samples ); -const fileToString = (path: string): string => fs.readFileSync(path).toString(); - -suite('Internal formatter', () => { - formatTests.forEach((formatTest) => { - test(`${formatTest.filenameRoot} internal format`, async () => { - // Arrange - const inFilePath = path.join( - filesParentPath, - `${formatTest.filenameRoot}${inFilenameSuffix}`, - ); - const inFileString = fileToString(inFilePath); - const outFilePath = path.join( - filesParentPath, - `${formatTest.filenameRoot}${outFilenameSuffix}`, - ); - const outFileString = fileToString(outFilePath); - const options = { ...defaultOptions, ...formatTest.options }; - - // Act - const actual = internalFormat(inFileString, options); - - // Assert - assert.strictEqual(actual, outFileString); - }); - }); -}); - suite('External formatter', () => { // test external formatter a few times to make sure the connection is working // advanced tests are for internal formatter only diff --git a/src/providers/formattingProvider.ts b/src/providers/formattingProvider.ts index d91f8b43..8b193f32 100644 --- a/src/providers/formattingProvider.ts +++ b/src/providers/formattingProvider.ts @@ -1,20 +1,6 @@ -import { isDeepStrictEqual } from 'util'; import * as vscode from 'vscode'; -import { commentRegExp } from '../common/constants'; import { ConfigKey, Global } from '../common/global'; -import { FormatOptions } from './formattingProvider.types'; -import { - alignSingleLineComments, - alignTextAssignOperator, - braceNumber, - buildIndentedLine, - documentToString, - FlowOfControlNestDepth, - nextLineIsOneCommandCode, - purify, - removeEmptyLines, - trimExtraSpaces, -} from './formattingProvider.utils'; +import { documentToString, internalFormat } from './formattingProvider.utils'; function fullDocumentRange(document: vscode.TextDocument): vscode.Range { const lastLineId = document.lineCount - 1; @@ -26,949 +12,6 @@ function fullDocumentRange(document: vscode.TextDocument): vscode.Range { ); } -export const internalFormat = ( - stringToFormat: string, - options: FormatOptions, -): string => { - let formattedString = ''; - - // INDENTATION - /** Current level of indentation. 0 = top-level, no indentation. */ - let depth = 0; - /** Level of indentation on previous line */ - let prevLineDepth = 0; - /** - * It's marker for `Return`, `ExitApp`, `Hotkeys` and `Labels`, - * which allow/disallow for them to be un-indented. - * - * ------------------------------------------------------------------------- - * `tagDepth === 0`: - * - * Indentation level was decreased by `Return`, `ExitApp`, `#If Directive`, - * so they placed on same indentation level as `Label`. - * - * Decrement of indentation level by `Label` is disallowed (previous - * `Label` finished with `Return` or `ExitApp` command and un-indent for - * fall-through scenario not needed). - * - * ------------------------------------------------------------------------- - * `tagDepth === depth`: - * - * Current indentation level is in sync with `Label` indentation level - * (no additional indent added by block `{}`, `oneCommandCode`, etc...). - * - * `Return`, `ExitApp`, `Fall-Through Label` allowed to be un-indented, - * so they will be placed on same indentation level as `Label`. - * - * `Label` allowed to be un-indented for fall-through scenario. - * - * ------------------------------------------------------------------------- - * `tagDepth !== depth`: - * - * `Return`, `ExitApp`, `Label` disallowed to be un-indented, so they - * will obey indentation rules as code above them (`Return` inside - * function, block `{}`, `oneCommandCode`, etc... stay on same - * indentation level as code above them). - * - * ------------------------------------------------------------------------- - * `tagDepth > 0` : - * - * `#If Directive` allowed to be un-indented by `tagDepth` value (jump - * several indentation levels). - * - * ------------------------------------------------------------------------- - * `tagDepth = depth`: - * - * Only `Label` makes syncing `tagDepth` with `depth`. - * - * `Case:` and `Default:` must not make syncing to disallow `Return`, - * `ExitApp` and `Label` to un-indent inside `Switch-Case` block. - * - * ------------------------------------------------------------------------- - * `tagDepth = 0`: - * - * `Return`, `ExitApp`, `#If Directive`, `HotkeySingleLine` resets - * `tagDepth` value, when they un-indented. - */ - let tagDepth = 0; - - // FLOW OF CONTROL and IF-ELSE nesting - /** - * `True` if this line is an one-statement block. Example: - * ```ahk - * if (var) ; false - * MsgBox ; true - * SoundBeep ; false - * ``` - */ - let oneCommandCode = false; - /** Previous line is one command code */ - let prevLineIsOneCommandCode = false; - /** - * Detect or not detect `oneCommandCode`. - * Every iteration it's `true`, but becomes `false` when formatter increase - * indent for next line by open brace `{`. - * It's prevent wrong extra indent, when `{` present after flow of control - * statement: one indent for `{` and additional indent for `oneCommandCode`. - */ - let detectOneCommandCode = true; - /** - * Object with array of indent level of `if` not completed by `else`. - * - * Allow us to de-indent (jump several indentation levels) `else` to last - * not complete `if` and de-indent (jump several indentation levels) code - * that exit nested flow of control statements inside block of code `{}`. - * - * Every time we meet `{` we `push` delimiter `-1` to array. - * - * Every time we meet `}` we delete last delimiter `-1` from array and all - * elements after it. - * - * Every time we meet `if` we `push` current `depth` to array. - * - * Every time we meet `else` we `pop` element from array. - * - * When code leaves `if` nesting we `splice` (delete) element(s) in array - * after last delimiter. - * - * Example: - * ```ahk - * ; [-1] - * if (var) ; [-1, 0] - * { ; [-1, 0, -1] - * if (var) ; [-1, 0, -1, 1] - * if (var) ; [-1, 0, -1, 1, 2] - * code - * else ; [-1, 0, -1, 1], de-indent to last not complete IF, - * code ; complete it by deleting last element - * code ; [-1, 0, -1] de-indent to first not complete IF inside - * } ; [-1, 0] code block - * code ; [-1] - * ``` - */ - let ifDepth = new FlowOfControlNestDepth(); - /** - * Object with array of indent level of first flow of control statement - * without open brace `{` with nested code inside it. - * - * Allow us to de-indent (jump several indentation levels) code - * that exit nested flow of control statements inside block of code `{}`. - * - * Every time we meet `{` we check did we add by mistake `depth` of last - * flow of control, if yes - revert changes, `push` delimiter `-1` to array. - * - * Every time we meet `}` we delete last delimiter `-1` and all elements - * after it. - * - * Every time we meet flow of control statement without `{` we `push` - * current `depth` to array (only if last element is delimiter). - * - * When code leaves flow of control nesting we `splice` (delete) element(s) - * in array after last delimiter. - * - * Example: - * ```ahk - * ; [-1] - * loop ; [-1, 0] added by mistake - * { ; [-1, -1] revert changes and add delimiter - * loop ; [-1, -1, 1] - * loop ; [-1, -1, 1] not added - * code - * code ; [-1, -1] de-indent to first flow of control inside - * } ; [-1] code block - * code ; [-1] - * ``` - */ - let focDepth = new FlowOfControlNestDepth(); - - // ALIGN ASSIGNMENT - /** - * Formatter's directive: - * ```ahk - * ;@AHK++AlignAssignmentOn - * ;@AHK++AlignAssignmentOff - * ``` - * Align assignment between this directives - */ - let alignAssignment = false; - /** Code block with assignment to be aligned */ - let assignmentBlock: string[] = []; - - // CONTINUATION SECTION - /** - * Continuation section: Expression, Object - * ```ahk - * obj := { a: 1 ; false - * , b: 2 } ; true - * if a = 1 ; false - * and b = 2 ; true - * ``` - */ - let continuationSectionExpression = false; - /** - * True iff continuation section is for text and should be formatted - * ```ahk - * ( LTrim - * Indented line of text - * ) - * ``` - */ - let continuationSectionTextFormat = false; - /** - * True iff continuation section is for text but should **not** be formatted - * ```ahk - * ( [NO LTrim option!] - * Line of text with preserved user formatting - * ) - * ``` - */ - let continuationSectionTextNotFormat = false; - /** - * Level of indentation of current line increased by open brace `{`, but not - * inside expression continuation section. - */ - let openBraceIndent = false; - /** - * The indentation of `oneCommandCode` is delayed, because the current line - * is an expression continuation section. The indentation is delayed by - * temporarily disabling `oneCommandCode`. - */ - let deferredOneCommandCode = false; - /** - * Indent level of open brace `{` that belongs to object's initialization - * with continuation section. - */ - let openBraceObjectDepth = -1; - - // BLOCK COMMENT - /** This line is block comment */ - let blockComment = false; - /** Base indent, that block comment had in original code */ - let blockCommentIndent = ''; - /** - * Formatter's directive: - * ```ahk - * ;@AHK++FormatBlockCommentOn - * ;@AHK++FormatBlockCommentOff - * ``` - * Format text inside block comment like regular code - */ - let formatBlockComment = false; - // Save formatter state to this variables on enter of block comment and - // restore them on exit of block comment - let preBlockCommentDepth = 0; - let preBlockCommentTagDepth = 0; - let preBlockCommentPrevLineDepth = 0; - let preBlockCommentOneCommandCode = false; - let preBlockCommentIfDepth = new FlowOfControlNestDepth(); - let preBlockCommentFocDepth = new FlowOfControlNestDepth(); - - // SETTINGS' ALIASES - const indentCodeAfterLabel = options.indentCodeAfterLabel; - const indentCodeAfterIfDirective = options.indentCodeAfterIfDirective; - const trimSpaces = options.trimExtraSpaces; - - // REGULAR EXPRESSION - /** Formatter's directive `;@AHK++AlignAssignmentOn` */ - const ahkAlignAssignmentOn = /;\s*@AHK\+\+AlignAssignmentOn/i; - /** Formatter's directive `;@AHK++AlignAssignmentOff` */ - const ahkAlignAssignmentOff = /;\s*@AHK\+\+AlignAssignmentOff/i; - /** Formatter's directive `;@AHK++FormatBlockCommentOn` */ - const ahkFormatBlockCommentOn = /;\s*@AHK\+\+FormatBlockCommentOn/i; - /** Formatter's directive `;@AHK++FormatBlockCommentOff` */ - const ahkFormatBlockCommentOff = /;\s*@AHK\+\+FormatBlockCommentOff/i; - /** - * A line that starts with `and`, `or`, `||`, `&&`, a comma, or a period is - * automatically merged with the line directly above it (the same is true - * for all other expression operators except `++` and `--`). - * - * Skip `++`, `--`, block comments `/ *` and `* /` - */ - const continuationSection = - /^(((and|or|not)\b)|[\^!~?:&<>=.,|]|\+(?!\+)|-(?!-)|\/(?!\*)|\*(?!\/))/; - /** - * Label name may consist of any characters other than `space`, `tab`, - * `comma` and the escape character (`). Not ended by double colon `::`. - * - * Generally, aside from whitespace and comments, - * no other code can be written on the same line as a label. - * - * Example: `Label:` - */ - const label = /^[^\s\t,`]+(? { - const purifiedLine = purify(originalLine).toLowerCase(); - /** The line comment. Empty string if no line comment exists */ - const comment = commentRegExp.exec(originalLine)?.[0] ?? ''; - let formattedLine = originalLine.replace(commentRegExp, ''); // Remove single line comment - formattedLine = trimExtraSpaces(formattedLine, trimSpaces) // Remove extra spaces between words - .concat(comment) // Add removed single line comment back - .trim(); - /** Line is empty or this is a single line comment */ - const emptyLine = purifiedLine === ''; - - detectOneCommandCode = true; - - const openBraceNum = braceNumber(purifiedLine, '{'); - const closeBraceNum = braceNumber(purifiedLine, '}'); - - // ===================================================================== - // | THIS LINE | - // ===================================================================== - - // STOP DIRECTIVE for formatter - if (emptyLine) { - if (alignAssignment && comment.match(ahkAlignAssignmentOff)) { - alignAssignment = false; - assignmentBlock = alignTextAssignOperator(assignmentBlock); - // Save aligned block - assignmentBlock.forEach((alignedFormattedLine, index) => { - formattedString += buildIndentedLine( - // restore 'lineIndex' before 'assignmentBlock' and add - // 'index + 1' - lineIndex - assignmentBlock.length + index + 1, - lines.length, - alignedFormattedLine, - depth, - options, - ); - }); - assignmentBlock = []; - } - if (formatBlockComment && comment.match(ahkFormatBlockCommentOff)) { - formatBlockComment = false; - } - } - - // ALIGN ASSIGNMENT - if (alignAssignment) { - assignmentBlock.push(formattedLine); - if (lineIndex !== lines.length - 1) { - // skip to the next iteration - return; - } - // Save aligned block if we reach end of text, but didn't find stop - // directive ';@AHK++AlignAssignmentOff' - assignmentBlock.forEach((alignedFormattedLine, index) => { - formattedString += buildIndentedLine( - // restore 'lineIndex' before 'assignmentBlock' and add - // 'index + 1' - lineIndex - assignmentBlock.length + index + 1, - lines.length, - alignedFormattedLine, - depth, - options, - ); - }); - assignmentBlock = []; - } - - // BLOCK COMMENT - // The /* and */ symbols can be used to comment out an entire section, - // but only if the symbols appear at the beginning of a line (excluding - // whitespace), like in this example: - // /* - // MsgBox, This line is commented out (disabled). - // MsgBox, Common mistake: */ this does not end the comment. - // MsgBox, This line is commented out. - // */ - if (!blockComment && originalLine.match(/^\s*\/\*/)) { - // found start '/*' pattern - blockComment = true; - // Save first capture group (original indent) - blockCommentIndent = originalLine.match(/(^\s*)\/\*/)?.[1]; - if (formatBlockComment) { - // save indent values on block comment enter - preBlockCommentDepth = depth; - preBlockCommentTagDepth = tagDepth; - preBlockCommentPrevLineDepth = prevLineDepth; - preBlockCommentOneCommandCode = oneCommandCode; - preBlockCommentIfDepth = ifDepth; - preBlockCommentFocDepth = focDepth; - // reset indent values to default values - tagDepth = depth; - prevLineDepth = depth; - oneCommandCode = false; - ifDepth = new FlowOfControlNestDepth(); - focDepth = new FlowOfControlNestDepth(); - } - } - - // BLOCK COMMENT - if (blockComment) { - // Save block comment line only if user don't want format it content - if (!formatBlockComment) { - let blockCommentLine = ''; - if (originalLine.startsWith(blockCommentIndent)) { - blockCommentLine = originalLine.substring( - blockCommentIndent.length, - ); - } else { - blockCommentLine = originalLine; - } - formattedString += buildIndentedLine( - lineIndex, - lines.length, - blockCommentLine.trimEnd(), - depth, - options, - ); - } - if (originalLine.match(/^\s*\*\//)) { - // found end '*/' pattern - blockComment = false; - if (formatBlockComment) { - // restore indent values on block comment exit - depth = preBlockCommentDepth; - tagDepth = preBlockCommentTagDepth; - prevLineDepth = preBlockCommentPrevLineDepth; - oneCommandCode = preBlockCommentOneCommandCode; - ifDepth = preBlockCommentIfDepth; - focDepth = preBlockCommentFocDepth; - } - } - if (!formatBlockComment) { - return; - } - } - - // SINGLE LINE COMMENT - if ( - emptyLine && - // skip formatter's directives - !comment.match(ahkAlignAssignmentOn) && - !comment.match(ahkAlignAssignmentOff) && - !comment.match(ahkFormatBlockCommentOn) && - !comment.match(ahkFormatBlockCommentOff) - ) { - // save with zero indent (indent value don't matter here) - formattedString += buildIndentedLine( - lineIndex, - lines.length, - formattedLine, - 0, - options, - ); - return; - } - - // CONTINUATION SECTION: Text [Not Formatted] Start - // ( [NO LTrim option!] <-- check this START parenthesis - // Line of text with preserved user formatting - // ) - // Skip hotkey: (:: - if (purifiedLine.match(/^\((?!::)(?!.*\bltrim\b)/)) { - continuationSectionTextNotFormat = true; - } - - // CONTINUATION SECTION: Text [Not Formatted] Save with original indent - if (continuationSectionTextNotFormat) { - formattedString += originalLine.trimEnd() + '\n'; - // CONTINUATION SECTION: Text [Not Formatted] Stop - // ( [NO LTrim option!] - // Line of text with preserved user formatting - // ) <-- check this STOP parenthesis - if (purifiedLine.match(/^\)/)) { - continuationSectionTextNotFormat = false; - } - return; - } - - // CONTINUATION SECTION: Text [Formatted] Stop - // ( LTrim - // Line of text - // ) <-- check this STOP parenthesis - if (continuationSectionTextFormat && purifiedLine.match(/^\)/)) { - continuationSectionTextFormat = false; - depth--; - } - - // CONTINUATION SECTION: Text [Formatted] Save indented - if (continuationSectionTextFormat) { - formattedString += buildIndentedLine( - lineIndex, - lines.length, - originalLine.trim(), - depth, - options, - ); - return; - } - - // CONTINUATION SECTION: Expression, Object, Flow of Control nesting - // obj := { a: 1 - // , b: 2 } - // if a = 1 - // and b = 2 - if ( - purifiedLine.match(continuationSection) && - // skip Hotkeys:: and ::Hotstrings:: (they has '::') - !purifiedLine.match(/::/) - ) { - continuationSectionExpression = true; - // CONTINUATION SECTION: Object - // obj := { a: 1 - // , b: 2 <-- revert one! indent level after open brace or - // , c: 3 } multiply open braces - if (openBraceIndent) { - depth--; - openBraceObjectDepth = prevLineDepth; - } - // CONTINUATION SECTION: Expression - // if a = 1 - // or b = 2 <-- revert indent for oneCommandCode and make it - // MsgBox deferred - if (oneCommandCode) { - deferredOneCommandCode = true; - oneCommandCode = false; - prevLineIsOneCommandCode = false; - depth--; - } - // CONTINUATION SECTION: Flow of Control nesting - // Loop - // code ; previous line is one command code - // , code <-- restore oneCommandCode depth - // code - if (prevLineIsOneCommandCode) { - oneCommandCode = true; - depth++; - } - depth++; - } - - // CONTINUATION SECTION: Expression - Deferred oneCommandCode indent - // if a = 1 - // or b = 2 - // MsgBox <-- restore deferred oneCommandCode - if (deferredOneCommandCode && !continuationSectionExpression) { - deferredOneCommandCode = false; - oneCommandCode = true; - depth++; - } - - // CLOSE BRACE - if (closeBraceNum) { - // FLOW OF CONTROL - // Example (restore close brace depth): - // foo() { - // for - // if - // return - // } ; <-- de-indent from all nesting before loosing information - // about depth via focDepth.exitBlockOfCode() below - if (focDepth.last() > -1) { - depth = focDepth.last(); - } - ifDepth.exitBlockOfCode(closeBraceNum); - focDepth.exitBlockOfCode(closeBraceNum); - // CONTINUATION SECTION: Object - // obj := { a: 1 - // , b: 2 - // , c: 3 } <-- skip de-indent by brace in Continuation Section: Object - if (!continuationSectionExpression) { - depth -= closeBraceNum; - } - } - - // OPEN BRACE - if (openBraceNum) { - // ONE COMMAND CODE - // else - // Loop { <-- skip de-indent one command code with open brace - // code - if ( - (oneCommandCode || deferredOneCommandCode) && - !nextLineIsOneCommandCode(purifiedLine) - ) { - if (deferredOneCommandCode) { - // if (a = 4 - // and b = 5) { - // MsgBox <-- disable deferredOneCommandCode indent - // } - deferredOneCommandCode = false; - } else if (purifiedLine.match(/^{/)) { - // if (var) - // { <-- revert oneCommandCode indent for open brace - // MsgBox - // } - // if (var) - // obj := { key1: val1 <-- but not for object continuation - // , key2: val2 } section - oneCommandCode = false; - depth -= openBraceNum; - } - // FLOW OF CONTROL revert added by mistake - // Loop, %var% - // { <-- check open brace below flow of control statement - // code - // } - if (depth === focDepth.last()) { - focDepth.pop(); - } - } - } - - // FLOW OF CONTROL de-indent from all nesting - // if (a > 0 - // and b > 0) <-- skip continuation section - // code <-- skip one command code - // /* block comment */ <-- skip block comment - // code <-- de-indent - if ( - (ifDepth.last() > -1 || focDepth.last() > -1) && - !continuationSectionExpression && - !oneCommandCode && - (!blockComment || formatBlockComment) - ) { - // Else: <-- skip valid LABEL - if (purifiedLine.match(/^}? ?else\b(?!:)/)) { - // { - // if - // if - // loop - // loop - // code - // else <-- de-indent "ELSE" to last not complete "IF" - // code - // else <-- de-indent "ELSE" to last not complete "IF" - // code - // code - // } - depth = ifDepth.pop(); - } else if (!purifiedLine.match(/^{/) && !purifiedLine.match(/^}/)) { - // Example (skip irrelevant braces): - // if <-- relevant - // { <-- skip irrelevant - // code <-- relevant - // } <-- skip irrelevant - // code <-- relevant - // Example (main logic): - // if | loop - // if | loop - // code | code - // code | code <-- de-indent from all nesting - const restoreIfDepth: number | undefined = - ifDepth.restoreDepth(); - const restoreFocDepth: number | undefined = - focDepth.restoreDepth(); - if ( - restoreIfDepth !== undefined && - restoreFocDepth !== undefined - ) { - depth = Math.min(restoreIfDepth, restoreFocDepth); - } else { - depth = restoreIfDepth ?? restoreFocDepth; - } - } - } - - // #IF DIRECTIVE - // #IfWinActive WinTitle1 - // Hotkey:: - // #IfWinActive WinTitle2 <-- fall-through scenario for #IF DIRECTIVE - // Hotkey:: with parameters - // #If <-- de-indent #IF DIRECTIVE without parameters - if (purifiedLine.match('^' + sharpDirective + '\\b')) { - if (tagDepth > 0) { - depth -= tagDepth; - } else { - depth--; - } - } - - // Return, Exit, ExitApp - // Label: - // code - // Return <-- force de-indent by one level for labels - if ( - purifiedLine.match(/^(return|exit|exitapp)\b/) && - tagDepth === depth - ) { - tagDepth = 0; - depth--; - } - - // SWITCH-CASE-DEFAULT or LABEL: or HOTKEY:: - if (purifiedLine.match(switchCaseDefault)) { - // Case: or Default: - depth--; - } else if ( - purifiedLine.match(label) || - purifiedLine.match(hotkey) || - purifiedLine.match(hotkeySingleLine) - ) { - if (indentCodeAfterLabel) { - // Label: or Hotkey:: - // De-indent label or hotkey, if they not end with 'return' - // command. - // This is fall-through scenario. Example: - // Label1: <-- de-indent - // code - // Label2: <-- de-indent - // code - // return - // De-indent single-line hotkey, after label. - // This is fall-through scenario. Example: - // F1:: - // F2:: <-- de-indent - // F3:: foo() <-- de-indent - if (tagDepth === depth) { - depth--; - } - } - } - - // De-indent by label may produce negative 'depth', it's normal behavior - if (depth < 0) { - depth = 0; - } - if (preBlockCommentDepth < 0) { - preBlockCommentDepth = 0; - } - - prevLineDepth = depth; - - // Save indented line - formattedString += buildIndentedLine( - lineIndex, - lines.length, - formattedLine, - depth, - options, - ); - - // ===================================================================== - // | NEXT LINE | - // ===================================================================== - - // START DIRECTIVE for formatter - if (emptyLine) { - if (comment.match(ahkAlignAssignmentOn)) { - alignAssignment = true; - } else if (comment.match(ahkFormatBlockCommentOn)) { - formatBlockComment = true; - } - } - - // ONE COMMAND CODE - if ( - oneCommandCode && - // Don't change indentation on block comment after one command code. - // Change indentation inside block comment, if user wants to format - // block comment. - (!blockComment || formatBlockComment) - ) { - oneCommandCode = false; - prevLineIsOneCommandCode = true; - // FLOW OF CONTROL - // if (var) - // if (var) <-- don't de-indent nested flow of control statement - if (!nextLineIsOneCommandCode(purifiedLine)) { - depth--; - } - } else { - prevLineIsOneCommandCode = false; - } - - // FLOW OF CONTROL - // Loop, %var% <-- flow of control statement without open brace - // code - // code - if ( - nextLineIsOneCommandCode(purifiedLine) && - openBraceNum === 0 && - focDepth.last() === -1 - ) { - focDepth.push(depth); - } - - // IF-ELSE complete tracking - // if { <-- check IF - // code - // } else if { <-- check IF - // code - // } else if <-- check IF - // code - // else if <-- check IF - // code - // If: <-- skip valid LABEL - if (purifiedLine.match(/^(}? ?else )?if\b(?!:)/)) { - ifDepth.push(depth); - } - - // OPEN BRACE - if (openBraceNum) { - depth += openBraceNum; - // Do not detect 'oneCommandCode', because it will produce extra - // indent for next line like in example below: - // if { - // code <-- wrong extra indent by oneCommandCode - // code - // } - detectOneCommandCode = false; - // CONTINUATION SECTION: Nested Objects - if (!continuationSectionExpression) { - openBraceIndent = true; - } else { - openBraceIndent = false; - } - // FLOW OF CONTROL - ifDepth.enterBlockOfCode(openBraceNum); - focDepth.enterBlockOfCode(openBraceNum); - } else { - openBraceIndent = false; - } - - // #IF DIRECTIVE with parameters - // #If Expression <-- indent next line after '#IF DIRECTIVE' - // F1:: MsgBox Help - if ( - purifiedLine.match('^' + sharpDirective + '\\b.+') && - indentCodeAfterIfDirective - ) { - depth++; - tagDepth = 0; - } - - // SWITCH-CASE-DEFAULT or LABEL: or HOTKEY:: - if (purifiedLine.match(switchCaseDefault)) { - // Case: or Default: <-- indent next line - // code - depth++; - // Do not sync here 'tagDepth' with 'depth' to prevent 'Return' and - // 'ExitApp' to de-indent inside 'Switch-Case-Default' construction! - } else if (purifiedLine.match(label) || purifiedLine.match(hotkey)) { - if (indentCodeAfterLabel) { - // Label: or Hotkey:: <-- indent next line - // code - // Do this only if the LABEL is not inside a nested code - if (focDepth.depth.length === 1) { - depth++; - tagDepth = depth; - } - } - } else if (purifiedLine.match(hotkeySingleLine)) { - tagDepth = 0; - } - - // CONTINUATION SECTION: Expression, Object - if (continuationSectionExpression) { - continuationSectionExpression = false; - // Object - Check close braces of nested objects - // obj := { a: 1 - // , b : { c: 2 - // , d: 3 } } <-- multiply close braces in nested objects - if (closeBraceNum) { - depth -= closeBraceNum; - // obj := { a: 1 - // , b : { c: 2 - // , d: 3 } } <-- revert indent after last close brace - if (openBraceObjectDepth === depth) { - openBraceObjectDepth = -1; - depth++; - } - } - // Expression - De-indent next line - // isPositive := x > 0 - // and y > 0 <-- de-indent next line after continuation section - // x++ - depth--; - } - - // CONTINUATION SECTION: Text [Formatted] Start - // ( LTrim <-- check this START parenthesis - // Indented line of text - // ) - // Skip hotkey "open parenthesis" (:: - if (purifiedLine.match(/^\((?!::)(?=.*\bltrim\b)/)) { - continuationSectionTextFormat = true; - depth++; - } - - // ONE COMMAND CODE - // Loop, %var% <-- indent next line - // code - // code - if (detectOneCommandCode && nextLineIsOneCommandCode(purifiedLine)) { - oneCommandCode = true; - depth++; - } - - // DEBUG CONSOLE OUTPUT - if (lineIndex === lines.length - 1) { - if ( - !( - isDeepStrictEqual(ifDepth.depth, [-1]) || - isDeepStrictEqual(ifDepth.depth, [-1, 0]) - ) && - !( - isDeepStrictEqual(focDepth.depth, [-1]) || - isDeepStrictEqual(focDepth.depth, [-1, 0]) - ) - ) { - // If code is finished (number of open and close braces are - // equal, flow of control statements has code after one command - // code, etc...) arrays must be equal [-1] or [-1, 0]. Last zero - // in array stays, because formatter waits code after close - // brace, but instead reaches EOF. If not equal, syntax is - // incorrect of there is bug in formatter logic. - console.error( - [ - 'Internal formatter data:', - 'ifDepth:', - ifDepth.depth, - 'focDepth:', - focDepth.depth, - ].join('\n'), - ); - } - } - }); - - formattedString = alignSingleLineComments(formattedString, options); - - formattedString = removeEmptyLines( - formattedString, - options.allowedNumberOfEmptyLines, - ); - - return formattedString; -}; - export class FormatProvider implements vscode.DocumentFormattingEditProvider { public provideDocumentFormattingEdits( document: vscode.TextDocument, diff --git a/src/providers/formattingProvider.utils.test.ts b/src/providers/formattingProvider.utils.test.ts index 261da003..c21146f1 100644 --- a/src/providers/formattingProvider.utils.test.ts +++ b/src/providers/formattingProvider.utils.test.ts @@ -12,603 +12,597 @@ import { FlowOfControlNestDepth, hasMoreCloseParens, hasMoreOpenParens, + internalFormat, nextLineIsOneCommandCode, normalizeLineAssignOperator, purify, removeEmptyLines, trimExtraSpaces, } from './formattingProvider.utils'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { FormatOptions } from './formattingProvider.types'; -suite('FormattingProvider utils', () => { - // Default formatting options - const defaultFormattingOptions: { - insertSpaces: boolean; - tabSize: number; - preserveIndent: boolean; - } = { - insertSpaces: true, - tabSize: 4, - preserveIndent: false, - }; +// Default formatting options +const defaultFormattingOptions: { + insertSpaces: boolean; + tabSize: number; + preserveIndent: boolean; +} = { + insertSpaces: true, + tabSize: 4, + preserveIndent: false, +}; - suite('braceNum', () => { - interface TestBraceData { - in: string; - bc: BraceChar; - bn: number; - } - // List of test data - const dataList: TestBraceData[] = [ - // { - // in: , // input test string - // bc: , // brace character - // bn: , // brace number - // }, - { - in: '{}', - bc: '{', - bn: 0, - }, - { - in: '{', - bc: '{', - bn: 1, - }, - { - in: '{}{', - bc: '{', - bn: 1, - }, - { - in: '}', - bc: '}', - bn: 1, +suite('braceNum', () => { + interface TestBraceData { + in: string; + bc: BraceChar; + bn: number; + } + // List of test data + const dataList: TestBraceData[] = [ + // { + // in: , // input test string + // bc: , // brace character + // bn: , // brace number + // }, + { + in: '{}', + bc: '{', + bn: 0, + }, + { + in: '{', + bc: '{', + bn: 1, + }, + { + in: '{}{', + bc: '{', + bn: 1, + }, + { + in: '}', + bc: '}', + bn: 1, + }, + { + in: '{}}', + bc: '}', + bn: 1, + }, + ]; + dataList.forEach((data) => { + test( + data.bc + ": '" + data.in + "'" + ' => ' + data.bn.toString(), + () => { + assert.strictEqual(braceNumber(data.in, data.bc), data.bn); }, - { - in: '{}}', - bc: '}', - bn: 1, - }, - ]; - dataList.forEach((data) => { - test( - data.bc + ": '" + data.in + "'" + ' => ' + data.bn.toString(), - () => { - assert.strictEqual(braceNumber(data.in, data.bc), data.bn); - }, - ); - }); + ); }); +}); - suite('buildIndentationChars', () => { - // List of test data - const dataList = [ - // { - // dp: , // depth of indentation - // rs: , // expected result - // }, - { - dp: 0, - ...defaultFormattingOptions, - rs: '', - }, - { - dp: 1, - ...defaultFormattingOptions, - rs: ' ', - }, - { - dp: 2, - ...defaultFormattingOptions, - rs: ' ', - }, - { - dp: 1, - ...defaultFormattingOptions, - insertSpaces: false, - rs: '\t', - }, - { - dp: 2, - ...defaultFormattingOptions, - insertSpaces: false, - rs: '\t\t', +suite('buildIndentationChars', () => { + // List of test data + const dataList = [ + // { + // dp: , // depth of indentation + // rs: , // expected result + // }, + { + dp: 0, + ...defaultFormattingOptions, + rs: '', + }, + { + dp: 1, + ...defaultFormattingOptions, + rs: ' ', + }, + { + dp: 2, + ...defaultFormattingOptions, + rs: ' ', + }, + { + dp: 1, + ...defaultFormattingOptions, + insertSpaces: false, + rs: '\t', + }, + { + dp: 2, + ...defaultFormattingOptions, + insertSpaces: false, + rs: '\t\t', + }, + ]; + dataList.forEach((data) => { + test( + 'depth:' + + data.dp + + ' spaces:' + + data.insertSpaces.toString() + + " => '" + + data.rs.replace(/\t/g, '\\t') + + "'", + () => { + assert.strictEqual( + buildIndentationChars(data.dp, { + insertSpaces: data.insertSpaces, + tabSize: data.tabSize, + }), + data.rs, + ); }, - ]; - dataList.forEach((data) => { - test( - 'depth:' + - data.dp + - ' spaces:' + - data.insertSpaces.toString() + - " => '" + - data.rs.replace(/\t/g, '\\t') + - "'", - () => { - assert.strictEqual( - buildIndentationChars(data.dp, { - insertSpaces: data.insertSpaces, - tabSize: data.tabSize, - }), - data.rs, - ); - }, - ); - }); + ); }); +}); - suite('buildIndentedLine', () => { - // List of test data - const dataList = [ - // { - // dp: , // depth of indentation - // fl: , // formatted line - // rs: , // expected result - // }, - { - dp: 0, - fl: 'SoundBeep', - ...defaultFormattingOptions, - rs: 'SoundBeep', - }, - { - dp: 1, - fl: 'SoundBeep', - ...defaultFormattingOptions, - rs: ' SoundBeep', - }, - { - dp: 2, - fl: 'SoundBeep', - ...defaultFormattingOptions, - rs: ' SoundBeep', - }, - { - dp: 1, - fl: 'SoundBeep', - ...defaultFormattingOptions, - insertSpaces: false, - rs: '\tSoundBeep', - }, - { - dp: 2, - fl: 'SoundBeep', - ...defaultFormattingOptions, - insertSpaces: false, - rs: '\t\tSoundBeep', - }, - { - dp: 1, - fl: '', - ...defaultFormattingOptions, - preserveIndent: true, - rs: ' ', - }, - { - dp: 2, - fl: '', - ...defaultFormattingOptions, - insertSpaces: false, - preserveIndent: true, - rs: '\t\t', +suite('buildIndentedLine', () => { + // List of test data + const dataList = [ + // { + // dp: , // depth of indentation + // fl: , // formatted line + // rs: , // expected result + // }, + { + dp: 0, + fl: 'SoundBeep', + ...defaultFormattingOptions, + rs: 'SoundBeep', + }, + { + dp: 1, + fl: 'SoundBeep', + ...defaultFormattingOptions, + rs: ' SoundBeep', + }, + { + dp: 2, + fl: 'SoundBeep', + ...defaultFormattingOptions, + rs: ' SoundBeep', + }, + { + dp: 1, + fl: 'SoundBeep', + ...defaultFormattingOptions, + insertSpaces: false, + rs: '\tSoundBeep', + }, + { + dp: 2, + fl: 'SoundBeep', + ...defaultFormattingOptions, + insertSpaces: false, + rs: '\t\tSoundBeep', + }, + { + dp: 1, + fl: '', + ...defaultFormattingOptions, + preserveIndent: true, + rs: ' ', + }, + { + dp: 2, + fl: '', + ...defaultFormattingOptions, + insertSpaces: false, + preserveIndent: true, + rs: '\t\t', + }, + ]; + dataList.forEach((data) => { + test( + 'depth:' + + data.dp + + ' spaces:' + + data.insertSpaces.toString() + + ' preserveIndent:' + + data.preserveIndent.toString() + + " '" + + data.fl + + "' => '" + + data.rs.replace(/\t/g, '\\t') + + "'", + () => { + assert.strictEqual( + buildIndentedLine(0, 1, data.fl, data.dp, { + insertSpaces: data.insertSpaces, + tabSize: data.tabSize, + preserveIndent: data.preserveIndent, + }), + data.rs, + ); }, - ]; - dataList.forEach((data) => { - test( - 'depth:' + - data.dp + - ' spaces:' + - data.insertSpaces.toString() + - ' preserveIndent:' + - data.preserveIndent.toString() + - " '" + - data.fl + - "' => '" + - data.rs.replace(/\t/g, '\\t') + - "'", - () => { - assert.strictEqual( - buildIndentedLine(0, 1, data.fl, data.dp, { - insertSpaces: data.insertSpaces, - tabSize: data.tabSize, - preserveIndent: data.preserveIndent, - }), - data.rs, - ); - }, - ); - }); + ); }); +}); - suite('hasMoreCloseParens', () => { - // List of test data - const dataList = [ - // { - // in: , // input test string - // rs: , // expected result - // }, - { - in: ')', - rs: true, - }, - { - in: '()', - rs: false, - }, - { - in: '())', - rs: true, - }, - { - in: '(::', - rs: false, - }, - { - in: '', - rs: false, - }, - ]; - dataList.forEach((data) => { - test("'" + data.in + "'" + ' => ' + data.rs.toString(), () => { - assert.strictEqual(hasMoreCloseParens(data.in), data.rs); - }); +suite('hasMoreCloseParens', () => { + // List of test data + const dataList = [ + // { + // in: , // input test string + // rs: , // expected result + // }, + { + in: ')', + rs: true, + }, + { + in: '()', + rs: false, + }, + { + in: '())', + rs: true, + }, + { + in: '(::', + rs: false, + }, + { + in: '', + rs: false, + }, + ]; + dataList.forEach((data) => { + test("'" + data.in + "'" + ' => ' + data.rs.toString(), () => { + assert.strictEqual(hasMoreCloseParens(data.in), data.rs); }); }); +}); - suite('hasMoreOpenParens', () => { - // List of test data - const dataList = [ - // { - // in: , // input test string - // rs: , // expected result - // }, - { - in: '(', - rs: true, - }, - { - in: '()', - rs: false, - }, - { - in: '(()', - rs: true, - }, - { - in: '(::', - rs: true, - }, - { - in: '', - rs: false, - }, - ]; - dataList.forEach((data) => { - test("'" + data.in + "'" + ' => ' + data.rs.toString(), () => { - assert.strictEqual(hasMoreOpenParens(data.in), data.rs); - }); +suite('hasMoreOpenParens', () => { + // List of test data + const dataList = [ + // { + // in: , // input test string + // rs: , // expected result + // }, + { + in: '(', + rs: true, + }, + { + in: '()', + rs: false, + }, + { + in: '(()', + rs: true, + }, + { + in: '(::', + rs: true, + }, + { + in: '', + rs: false, + }, + ]; + dataList.forEach((data) => { + test("'" + data.in + "'" + ' => ' + data.rs.toString(), () => { + assert.strictEqual(hasMoreOpenParens(data.in), data.rs); }); }); +}); - suite('purify', () => { - // List of test data - const dataList = [ - // { - // in: , // input test string - // rs: , // expected result - // }, - { - in: 'foo("; not comment")', - rs: 'foo("")', - }, - { - in: 'MsgBox, { ; comment with close brace }', - rs: 'MsgBox', - }, - { - in: 'MsgBox % "; not comment"', - rs: 'MsgBox', - }, - { - in: 'str = "`; not comment"', - rs: 'str = ""', - }, - { - in: 'str = "; comment with double quote"', - rs: 'str = ""', - }, - { - in: 'str = "; comment', - rs: 'str = "; comment', - }, - { - in: 'str = " ; comment', - rs: 'str = "', - }, - { - in: 'str = " `; not comment', - rs: 'str = " `; not comment', - }, - { - in: 'Gui, %id%: Color, % color', - rs: 'Gui', - }, - { - in: 'Send(Gui)', - rs: 'Send(Gui)', - }, - { - in: 'Send(foo)', - rs: 'Send(foo)', - }, - { - in: 'foo(Gui)', - rs: 'foo(Gui)', - }, - { - in: '{}', - rs: '', - }, - { - in: '{}{}', - rs: '', - }, - { - in: 'a{b{c}d}e', - rs: 'ae', - }, - { - in: '{{}', - rs: '{', - }, - { - in: '{}}', - rs: '}', - }, - ]; - dataList.forEach((data) => { - test("'" + data.in + "' => '" + data.rs + "'", () => { - assert.strictEqual(purify(data.in), data.rs); - }); +suite('purify', () => { + // List of test data + const dataList = [ + // { + // in: , // input test string + // rs: , // expected result + // }, + { + in: 'foo("; not comment")', + rs: 'foo("")', + }, + { + in: 'MsgBox, { ; comment with close brace }', + rs: 'MsgBox', + }, + { + in: 'MsgBox % "; not comment"', + rs: 'MsgBox', + }, + { + in: 'str = "`; not comment"', + rs: 'str = ""', + }, + { + in: 'str = "; comment with double quote"', + rs: 'str = ""', + }, + { + in: 'str = "; comment', + rs: 'str = "; comment', + }, + { + in: 'str = " ; comment', + rs: 'str = "', + }, + { + in: 'str = " `; not comment', + rs: 'str = " `; not comment', + }, + { + in: 'Gui, %id%: Color, % color', + rs: 'Gui', + }, + { + in: 'Send(Gui)', + rs: 'Send(Gui)', + }, + { + in: 'Send(foo)', + rs: 'Send(foo)', + }, + { + in: 'foo(Gui)', + rs: 'foo(Gui)', + }, + { + in: '{}', + rs: '', + }, + { + in: '{}{}', + rs: '', + }, + { + in: 'a{b{c}d}e', + rs: 'ae', + }, + { + in: '{{}', + rs: '{', + }, + { + in: '{}}', + rs: '}', + }, + ]; + dataList.forEach((data) => { + test("'" + data.in + "' => '" + data.rs + "'", () => { + assert.strictEqual(purify(data.in), data.rs); }); }); +}); - suite('removeEmptyLines', () => { - // List of test data - const dataList = [ - // { - // in: , // input test string - // ln: , // allowed empty lines - // rs: , // expected result - // }, - { - in: 'text\n\n\n\n\ntext\n\n\n\n\n', - ln: -1, - rs: 'text\n\n\n\n\ntext\n\n\n\n\n', - }, - { - in: 'text\n\n\n\n\ntext\n\n\n\n\n', - ln: 0, - rs: 'text\ntext\n', - }, - { - in: 'text\n\n\n\n\ntext\n\n\n\n\n', - ln: 1, - rs: 'text\n\ntext\n\n', - }, - { - in: 'text\n\n\n\n\ntext\n\n\n\n\n', - ln: 2, - rs: 'text\n\n\ntext\n\n\n', +suite('removeEmptyLines', () => { + // List of test data + const dataList = [ + // { + // in: , // input test string + // ln: , // allowed empty lines + // rs: , // expected result + // }, + { + in: 'text\n\n\n\n\ntext\n\n\n\n\n', + ln: -1, + rs: 'text\n\n\n\n\ntext\n\n\n\n\n', + }, + { + in: 'text\n\n\n\n\ntext\n\n\n\n\n', + ln: 0, + rs: 'text\ntext\n', + }, + { + in: 'text\n\n\n\n\ntext\n\n\n\n\n', + ln: 1, + rs: 'text\n\ntext\n\n', + }, + { + in: 'text\n\n\n\n\ntext\n\n\n\n\n', + ln: 2, + rs: 'text\n\n\ntext\n\n\n', + }, + { + in: 'text\n\n\n\n\ntext\n\n\n\n\n', + ln: 3, + rs: 'text\n\n\n\ntext\n\n\n\n', + }, + { + in: 'text\n \n \n \n \ntext\n \n \n \n \n', + ln: 0, + rs: 'text\ntext\n', + }, + { + in: 'text\n \n \n \n \ntext\n \n \n \n \n', + ln: 1, + rs: 'text\n \ntext\n \n', + }, + { + in: 'text\n \n \n \n \ntext\n \n \n \n \n', + ln: 2, + rs: 'text\n \n \ntext\n \n \n', + }, + { + in: 'text\n \n \n \n \ntext\n \n \n \n \n', + ln: 3, + rs: 'text\n \n \n \ntext\n \n \n \n', + }, + { + in: '\n\n\ntext', + ln: 1, + rs: 'text', + }, + { + in: ' \n', + ln: 1, + rs: '', + }, + { + in: '\t\n', + ln: 1, + rs: '', + }, + { + in: 'text\ntext', + ln: 1, + rs: 'text\ntext', + }, + { + in: 'text\n', + ln: 1, + rs: 'text\n', + }, + { + // First empty line is \n -> use \n everywhere + in: 'a\r\n\n\r\n\n\nb', + ln: 2, + rs: 'a\r\n\n\nb', + }, + { + // First empty line is \r\n -> use \r\n everywhere + in: 'a\n\r\n\n\r\nb', + ln: 2, + rs: 'a\n\r\n\r\nb', + }, + { + // First empty line is \r\n -> use \r\n everywhere + // Even though we have not exceeded count of empty lines + in: 'a\n\r\n\n\r\nb', + ln: 3, + rs: 'a\n\r\n\r\n\r\nb', + }, + { + // 4 lines allowed, 3 found + // Make no change since we have not met or exceeded allowed count + in: 'a\n\r\n\n\r\nb', + ln: 4, + rs: 'a\n\r\n\n\r\nb', + }, + ]; + dataList.forEach((data) => { + test( + 'ln:' + + data.ln + + " '" + + data.in.replace(/\r/g, '\\r').replace(/\n/g, '\\n') + + "' => '" + + data.rs.replace(/\r/g, '\\r').replace(/\n/g, '\\n') + + "'", + () => { + assert.strictEqual(removeEmptyLines(data.in, data.ln), data.rs); }, - { - in: 'text\n\n\n\n\ntext\n\n\n\n\n', - ln: 3, - rs: 'text\n\n\n\ntext\n\n\n\n', - }, - { - in: 'text\n \n \n \n \ntext\n \n \n \n \n', - ln: 0, - rs: 'text\ntext\n', - }, - { - in: 'text\n \n \n \n \ntext\n \n \n \n \n', - ln: 1, - rs: 'text\n \ntext\n \n', - }, - { - in: 'text\n \n \n \n \ntext\n \n \n \n \n', - ln: 2, - rs: 'text\n \n \ntext\n \n \n', - }, - { - in: 'text\n \n \n \n \ntext\n \n \n \n \n', - ln: 3, - rs: 'text\n \n \n \ntext\n \n \n \n', - }, - { - in: '\n\n\ntext', - ln: 1, - rs: 'text', - }, - { - in: ' \n', - ln: 1, - rs: '', - }, - { - in: '\t\n', - ln: 1, - rs: '', - }, - { - in: 'text\ntext', - ln: 1, - rs: 'text\ntext', - }, - { - in: 'text\n', - ln: 1, - rs: 'text\n', - }, - { - // First empty line is \n -> use \n everywhere - in: 'a\r\n\n\r\n\n\nb', - ln: 2, - rs: 'a\r\n\n\nb', - }, - { - // First empty line is \r\n -> use \r\n everywhere - in: 'a\n\r\n\n\r\nb', - ln: 2, - rs: 'a\n\r\n\r\nb', - }, - { - // First empty line is \r\n -> use \r\n everywhere - // Even though we have not exceeded count of empty lines - in: 'a\n\r\n\n\r\nb', - ln: 3, - rs: 'a\n\r\n\r\n\r\nb', - }, - { - // 4 lines allowed, 3 found - // Make no change since we have not met or exceeded allowed count - in: 'a\n\r\n\n\r\nb', - ln: 4, - rs: 'a\n\r\n\n\r\nb', - }, - ]; - dataList.forEach((data) => { - test( - 'ln:' + - data.ln + - " '" + - data.in.replace(/\r/g, '\\r').replace(/\n/g, '\\n') + - "' => '" + - data.rs.replace(/\r/g, '\\r').replace(/\n/g, '\\n') + - "'", - () => { - assert.strictEqual( - removeEmptyLines(data.in, data.ln), - data.rs, - ); - }, - ); - }); + ); }); +}); - suite('trimExtraSpaces', () => { - // List of test data - const dataList = [ - // { - // in: , // input test string - // rs: , // expected result - // ts: , // trim extra spaces - // }, - { - in: 'InputFile := "movie.mkv"', - rs: 'InputFile := "movie.mkv"', - ts: true, - }, - { - in: 'InputFile := "movie.mkv"', - rs: 'InputFile := "movie.mkv"', - ts: false, +suite('trimExtraSpaces', () => { + // List of test data + const dataList = [ + // { + // in: , // input test string + // rs: , // expected result + // ts: , // trim extra spaces + // }, + { + in: 'InputFile := "movie.mkv"', + rs: 'InputFile := "movie.mkv"', + ts: true, + }, + { + in: 'InputFile := "movie.mkv"', + rs: 'InputFile := "movie.mkv"', + ts: false, + }, + { + in: 'MsgBox, 4, , testing testing', + rs: 'MsgBox, 4, , testing testing', + ts: true, + }, + ]; + dataList.forEach((data) => { + test( + 'Trim(' + + data.ts.toString() + + "): '" + + data.in + + "' => '" + + data.rs + + "'", + () => { + assert.strictEqual(trimExtraSpaces(data.in, data.ts), data.rs); }, - { - in: 'MsgBox, 4, , testing testing', - rs: 'MsgBox, 4, , testing testing', - ts: true, - }, - ]; - dataList.forEach((data) => { - test( - 'Trim(' + - data.ts.toString() + - "): '" + - data.in + - "' => '" + - data.rs + - "'", - () => { - assert.strictEqual( - trimExtraSpaces(data.in, data.ts), - data.rs, - ); - }, - ); - }); + ); }); +}); - suite('normalizeLineAssignOperator', () => { - // List of test data - const dataList = [ - // { - // in: , // input test string - // rs: , // expected result - // }, - { - in: 'a = 5 ; beautiful operator =', - rs: 'a = 5 ', - }, - { - in: 'abc=text', - rs: 'abc = text', - }, - { - in: 'InputFile := "movie.mkv"', - rs: 'InputFile := "movie.mkv"', - }, - { - in: 'a := 5 ; beautiful operator :=', - rs: 'a := 5 ', - }, - { - in: 'abc:="text"', - rs: 'abc := "text"', - }, - { - in: 'abc:=a + b', - rs: 'abc := a + b', - }, - { - in: '; beautiful operator :=', - rs: '', - }, - { - in: 'ToolTip, text', - rs: 'ToolTip, text', - }, - { - in: 'x := "1+1=2"', - rs: 'x := "1+1=2"', - }, - { - in: 'val = "="', - rs: 'val = "="', - }, - { - in: 'withSpaces = "x = y"', - rs: 'withSpaces = "x = y"', - }, - { - in: ' IndentedVariableWithTrailSpaces = movie.mkv ', - rs: ' IndentedVariableWithTrailSpaces = movie.mkv ', - }, - ]; - dataList.forEach((data) => { - test("'" + data.in + "' => '" + data.rs + "'", () => { - assert.strictEqual( - normalizeLineAssignOperator(data.in), - data.rs, - ); - }); +suite('normalizeLineAssignOperator', () => { + // List of test data + const dataList = [ + // { + // in: , // input test string + // rs: , // expected result + // }, + { + in: 'a = 5 ; beautiful operator =', + rs: 'a = 5 ', + }, + { + in: 'abc=text', + rs: 'abc = text', + }, + { + in: 'InputFile := "movie.mkv"', + rs: 'InputFile := "movie.mkv"', + }, + { + in: 'a := 5 ; beautiful operator :=', + rs: 'a := 5 ', + }, + { + in: 'abc:="text"', + rs: 'abc := "text"', + }, + { + in: 'abc:=a + b', + rs: 'abc := a + b', + }, + { + in: '; beautiful operator :=', + rs: '', + }, + { + in: 'ToolTip, text', + rs: 'ToolTip, text', + }, + { + in: 'x := "1+1=2"', + rs: 'x := "1+1=2"', + }, + { + in: 'val = "="', + rs: 'val = "="', + }, + { + in: 'withSpaces = "x = y"', + rs: 'withSpaces = "x = y"', + }, + { + in: ' IndentedVariableWithTrailSpaces = movie.mkv ', + rs: ' IndentedVariableWithTrailSpaces = movie.mkv ', + }, + ]; + dataList.forEach((data) => { + test("'" + data.in + "' => '" + data.rs + "'", () => { + assert.strictEqual(normalizeLineAssignOperator(data.in), data.rs); }); }); +}); - suite('alignLineAssignOperator', () => { - // List of test data - /* +suite('alignLineAssignOperator', () => { + // List of test data + /* Input Data InputFile := "movie.mkv" a := 5 ; beautiful operator := @@ -622,435 +616,578 @@ suite('FormattingProvider utils', () => { abc := a + b ; beautiful operator := */ - const dataList = [ - // { - // in: , // input test string - // tp: , // target position - // rs: , // expected result - // }, - { - in: 'InputFile = movie.mkv', - rs: 'InputFile = movie.mkv', - tp: 10, - }, - { - in: 'a = 5 ; beautiful operator =', - rs: 'a = 5 ; beautiful operator =', - tp: 10, - }, - { - in: 'abc=text', - rs: 'abc = text', - tp: 10, - }, - { - in: 'InputFile := "movie.mkv" ', - rs: 'InputFile := "movie.mkv"', - tp: 11, - }, - { - in: 'a := 5 ; beautiful operator :=', - rs: 'a := 5 ; beautiful operator :=', - tp: 11, - }, - { - in: 'abc:=a + b', - rs: 'abc := a + b', - tp: 11, - }, - { - in: 'abc:="text"', - rs: 'abc := "text"', - tp: 11, - }, - { - in: '; beautiful operator :=', - rs: '; beautiful operator :=', - tp: 15, - }, - { - in: 'ToolTip, text', - rs: 'ToolTip, text', - tp: 15, - }, - { - in: ' IndentedVarWithTrailSpaces = movie.mkv ', - rs: ' IndentedVarWithTrailSpaces = movie.mkv', - tp: 31, - }, - ]; - dataList.forEach((data) => { - test("'" + data.in + "' => '" + data.rs + "'", () => { - assert.strictEqual( - alignLineAssignOperator(data.in, data.tp), - data.rs, - ); - }); + const dataList = [ + // { + // in: , // input test string + // tp: , // target position + // rs: , // expected result + // }, + { + in: 'InputFile = movie.mkv', + rs: 'InputFile = movie.mkv', + tp: 10, + }, + { + in: 'a = 5 ; beautiful operator =', + rs: 'a = 5 ; beautiful operator =', + tp: 10, + }, + { + in: 'abc=text', + rs: 'abc = text', + tp: 10, + }, + { + in: 'InputFile := "movie.mkv" ', + rs: 'InputFile := "movie.mkv"', + tp: 11, + }, + { + in: 'a := 5 ; beautiful operator :=', + rs: 'a := 5 ; beautiful operator :=', + tp: 11, + }, + { + in: 'abc:=a + b', + rs: 'abc := a + b', + tp: 11, + }, + { + in: 'abc:="text"', + rs: 'abc := "text"', + tp: 11, + }, + { + in: '; beautiful operator :=', + rs: '; beautiful operator :=', + tp: 15, + }, + { + in: 'ToolTip, text', + rs: 'ToolTip, text', + tp: 15, + }, + { + in: ' IndentedVarWithTrailSpaces = movie.mkv ', + rs: ' IndentedVarWithTrailSpaces = movie.mkv', + tp: 31, + }, + ]; + dataList.forEach((data) => { + test("'" + data.in + "' => '" + data.rs + "'", () => { + assert.strictEqual( + alignLineAssignOperator(data.in, data.tp), + data.rs, + ); }); }); +}); - suite('FlowOfControlNestDepth.enterBlockOfCode', () => { - // List of test data - const dataList = [ - // { - // in: , // input array - // bn: , // brace number - // rs: , // expected result - // }, - { - in: new FlowOfControlNestDepth([-1]), - bn: 1, - rs: [-1, -1], - }, - { - in: new FlowOfControlNestDepth([-1]), - bn: 2, - rs: [-1, -1, -1], - }, - ]; - dataList.forEach((data) => { - test('[' + data.in.depth + '] => [' + data.rs + ']', () => { - assert.deepStrictEqual( - data.in.enterBlockOfCode(data.bn), - data.rs, - ); - }); +suite('FlowOfControlNestDepth.enterBlockOfCode', () => { + // List of test data + const dataList = [ + // { + // in: , // input array + // bn: , // brace number + // rs: , // expected result + // }, + { + in: new FlowOfControlNestDepth([-1]), + bn: 1, + rs: [-1, -1], + }, + { + in: new FlowOfControlNestDepth([-1]), + bn: 2, + rs: [-1, -1, -1], + }, + ]; + dataList.forEach((data) => { + test('[' + data.in.depth + '] => [' + data.rs + ']', () => { + assert.deepStrictEqual(data.in.enterBlockOfCode(data.bn), data.rs); }); }); +}); - suite('FlowOfControlNestDepth.exitBlockOfCode', () => { - // List of test data - const dataList = [ - // { - // in: , // input array - // bn: , // brace number - // rs: , // expected result - // }, - { - in: new FlowOfControlNestDepth([-1, 0, -1, 1, 2]), - bn: 1, - rs: [-1, 0], - }, - { - in: new FlowOfControlNestDepth([-1, 0, -1, 1, 2]), - bn: 2, - rs: [-1], - }, - ]; - dataList.forEach((data) => { - test('[' + data.in.depth + '] => [' + data.rs + ']', () => { - assert.deepStrictEqual( - data.in.exitBlockOfCode(data.bn), - data.rs, - ); - }); +suite('FlowOfControlNestDepth.exitBlockOfCode', () => { + // List of test data + const dataList = [ + // { + // in: , // input array + // bn: , // brace number + // rs: , // expected result + // }, + { + in: new FlowOfControlNestDepth([-1, 0, -1, 1, 2]), + bn: 1, + rs: [-1, 0], + }, + { + in: new FlowOfControlNestDepth([-1, 0, -1, 1, 2]), + bn: 2, + rs: [-1], + }, + ]; + dataList.forEach((data) => { + test('[' + data.in.depth + '] => [' + data.rs + ']', () => { + assert.deepStrictEqual(data.in.exitBlockOfCode(data.bn), data.rs); }); }); +}); - suite('FlowOfControlNestDepth.pop', () => { - // List of test data - const dataList = [ - // { - // in: , // input array - // rs: , // expected result - // }, - { - in: new FlowOfControlNestDepth([-1]), - rs: [-1], - }, - ]; - dataList.forEach((data) => { - test('[' + data.in.depth + '] => [' + data.rs + ']', () => { - data.in.pop(); - assert.deepStrictEqual(data.in.depth, data.rs); - }); +suite('FlowOfControlNestDepth.pop', () => { + // List of test data + const dataList = [ + // { + // in: , // input array + // rs: , // expected result + // }, + { + in: new FlowOfControlNestDepth([-1]), + rs: [-1], + }, + ]; + dataList.forEach((data) => { + test('[' + data.in.depth + '] => [' + data.rs + ']', () => { + data.in.pop(); + assert.deepStrictEqual(data.in.depth, data.rs); }); }); +}); - suite('FlowOfControlNestDepth.restoreEmptyDepth', () => { - // List of test data - const dataList = [ - // { - // in: , // input array - // rs: , // expected result - // }, - { - in: new FlowOfControlNestDepth([]), - rs: [-1], - }, - ]; - dataList.forEach((data) => { - test('[' + data.in.depth + '] => [' + data.rs + ']', () => { - data.in.restoreEmptyDepth(); - assert.deepStrictEqual(data.in.depth, data.rs); - }); +suite('FlowOfControlNestDepth.restoreEmptyDepth', () => { + // List of test data + const dataList = [ + // { + // in: , // input array + // rs: , // expected result + // }, + { + in: new FlowOfControlNestDepth([]), + rs: [-1], + }, + ]; + dataList.forEach((data) => { + test('[' + data.in.depth + '] => [' + data.rs + ']', () => { + data.in.restoreEmptyDepth(); + assert.deepStrictEqual(data.in.depth, data.rs); }); }); +}); - suite('FlowOfControlNestDepth.restoreDepth', () => { - // List of test data - const dataList = [ - // { - // in: , // input array - // rs: , // expected result - // dp: , // depth - // }, - { - in: new FlowOfControlNestDepth([-1, 0, -1, 1, 2]), - rs: 1, - dp: [-1, 0, -1], - }, - { - in: new FlowOfControlNestDepth([-1]), - rs: undefined, - dp: [-1], - }, - ]; - dataList.forEach((data) => { - test('[' + data.in.depth + "] => '" + data.rs + "'", () => { - assert.strictEqual(data.in.restoreDepth(), data.rs); - }); - test('[' + data.in.depth + '] => [' + data.dp + ']', () => { - data.in.restoreDepth(); - assert.deepStrictEqual(data.in.depth, data.dp); - }); +suite('FlowOfControlNestDepth.restoreDepth', () => { + // List of test data + const dataList = [ + // { + // in: , // input array + // rs: , // expected result + // dp: , // depth + // }, + { + in: new FlowOfControlNestDepth([-1, 0, -1, 1, 2]), + rs: 1, + dp: [-1, 0, -1], + }, + { + in: new FlowOfControlNestDepth([-1]), + rs: undefined, + dp: [-1], + }, + ]; + dataList.forEach((data) => { + test('[' + data.in.depth + "] => '" + data.rs + "'", () => { + assert.strictEqual(data.in.restoreDepth(), data.rs); + }); + test('[' + data.in.depth + '] => [' + data.dp + ']', () => { + data.in.restoreDepth(); + assert.deepStrictEqual(data.in.depth, data.dp); }); }); +}); - suite('alignSingleLineComments', () => { - // List of test data - const dataList = [ - // { - // in: , // input test string - // rs: , // expected result - // }, - { - in: '', - ...defaultFormattingOptions, - preserveIndent: true, - rs: '', - }, - { - in: 'MsgBox', - ...defaultFormattingOptions, - rs: 'MsgBox', - }, - { - in: 'MsgBox\n', - ...defaultFormattingOptions, - rs: 'MsgBox\n', - }, - { - in: ';comment\nMsgBox', - ...defaultFormattingOptions, - rs: ';comment\nMsgBox', - }, - { - in: ';comment\n MsgBox', - ...defaultFormattingOptions, - rs: ' ;comment\n MsgBox', - }, - { - in: ';comment\n\tMsgBox', - ...defaultFormattingOptions, - insertSpaces: false, - rs: '\t;comment\n\tMsgBox', - }, - { - in: ';comment\n}\nMsgBox', - ...defaultFormattingOptions, - rs: ' ;comment\n}\nMsgBox', - }, - { - in: ';comment\n , a: 4 }', - ...defaultFormattingOptions, - rs: ' ;comment\n , a: 4 }', - }, - { - in: '\n MsgBox', - ...defaultFormattingOptions, - preserveIndent: true, - rs: ' \n MsgBox', - }, - { - in: '\n\tMsgBox', - ...defaultFormattingOptions, - insertSpaces: false, - preserveIndent: true, - rs: '\t\n\tMsgBox', - }, - ]; - dataList.forEach((data) => { - test("'" + data.in + "'" + ' => ' + data.rs.toString(), () => { - assert.strictEqual( - alignSingleLineComments(data.in, { - insertSpaces: data.insertSpaces, - tabSize: data.tabSize, - preserveIndent: data.preserveIndent, - }), - data.rs, - ); - }); +suite('alignSingleLineComments', () => { + // List of test data + const dataList = [ + // { + // in: , // input test string + // rs: , // expected result + // }, + { + in: '', + ...defaultFormattingOptions, + preserveIndent: true, + rs: '', + }, + { + in: 'MsgBox', + ...defaultFormattingOptions, + rs: 'MsgBox', + }, + { + in: 'MsgBox\n', + ...defaultFormattingOptions, + rs: 'MsgBox\n', + }, + { + in: ';comment\nMsgBox', + ...defaultFormattingOptions, + rs: ';comment\nMsgBox', + }, + { + in: ';comment\n MsgBox', + ...defaultFormattingOptions, + rs: ' ;comment\n MsgBox', + }, + { + in: ';comment\n\tMsgBox', + ...defaultFormattingOptions, + insertSpaces: false, + rs: '\t;comment\n\tMsgBox', + }, + { + in: ';comment\n}\nMsgBox', + ...defaultFormattingOptions, + rs: ' ;comment\n}\nMsgBox', + }, + { + in: ';comment\n , a: 4 }', + ...defaultFormattingOptions, + rs: ' ;comment\n , a: 4 }', + }, + { + in: '\n MsgBox', + ...defaultFormattingOptions, + preserveIndent: true, + rs: ' \n MsgBox', + }, + { + in: '\n\tMsgBox', + ...defaultFormattingOptions, + insertSpaces: false, + preserveIndent: true, + rs: '\t\n\tMsgBox', + }, + ]; + dataList.forEach((data) => { + test("'" + data.in + "'" + ' => ' + data.rs.toString(), () => { + assert.strictEqual( + alignSingleLineComments(data.in, { + insertSpaces: data.insertSpaces, + tabSize: data.tabSize, + preserveIndent: data.preserveIndent, + }), + data.rs, + ); }); }); +}); - suite('calculateDepth', () => { - // List of test data - const dataList = [ - // { - // in: , // input test string - // rs: , // expected result - // }, - { - in: '', - ...defaultFormattingOptions, - rs: 0, - }, - { - in: 'MsgBox', - ...defaultFormattingOptions, - rs: 0, - }, - { - in: ' MsgBox', - ...defaultFormattingOptions, - rs: 2, - }, - { - in: '\t\tMsgBox', - ...defaultFormattingOptions, - insertSpaces: false, - rs: 2, - }, - ]; - dataList.forEach((data) => { - test("'" + data.in + "'" + ' => ' + data.rs.toString(), () => { - assert.strictEqual( - calculateDepth(data.in, { - insertSpaces: data.insertSpaces, - tabSize: data.tabSize, - }), - data.rs, - ); - }); +suite('calculateDepth', () => { + // List of test data + const dataList = [ + // { + // in: , // input test string + // rs: , // expected result + // }, + { + in: '', + ...defaultFormattingOptions, + rs: 0, + }, + { + in: 'MsgBox', + ...defaultFormattingOptions, + rs: 0, + }, + { + in: ' MsgBox', + ...defaultFormattingOptions, + rs: 2, + }, + { + in: '\t\tMsgBox', + ...defaultFormattingOptions, + insertSpaces: false, + rs: 2, + }, + ]; + dataList.forEach((data) => { + test("'" + data.in + "'" + ' => ' + data.rs.toString(), () => { + assert.strictEqual( + calculateDepth(data.in, { + insertSpaces: data.insertSpaces, + tabSize: data.tabSize, + }), + data.rs, + ); }); }); +}); - suite('nextLineIsOneCommandCode', () => { - // List of test data - const dataList = [ - // { - // in: , // input test string - // rs: , // expected result - // }, - { - in: 'else', - rs: true, - }, - { - in: '}else', - rs: true, - }, - { - in: '} else', - rs: true, - }, - { - in: 'else{', - rs: true, - }, - { - in: 'else {', - rs: true, - }, - { - in: 'Else:', - rs: false, - }, - ]; - dataList.forEach((data) => { - test("'" + data.in + "'" + ' => ' + data.rs.toString(), () => { - assert.strictEqual(nextLineIsOneCommandCode(data.in), data.rs); - }); +suite('nextLineIsOneCommandCode', () => { + // List of test data + const dataList = [ + // { + // in: , // input test string + // rs: , // expected result + // }, + { + in: 'else', + rs: true, + }, + { + in: '}else', + rs: true, + }, + { + in: '} else', + rs: true, + }, + { + in: 'else{', + rs: true, + }, + { + in: 'else {', + rs: true, + }, + { + in: 'Else:', + rs: false, + }, + ]; + dataList.forEach((data) => { + test("'" + data.in + "'" + ' => ' + data.rs.toString(), () => { + assert.strictEqual(nextLineIsOneCommandCode(data.in), data.rs); }); }); +}); - // Internal tests mock VS Code behavior to isolate flaws and run faster - suite('internal documentToString', () => { - const myTests = [ - { - name: '0 lines (empty string)', - in: { - lineCount: 0, - lineAt(): { text: string } { - throw new Error('Argument out of bounds'); - }, +// Internal tests mock VS Code behavior to isolate flaws and run faster +suite('internal documentToString', () => { + const myTests = [ + { + name: '0 lines (empty string)', + in: { + lineCount: 0, + lineAt(): { text: string } { + throw new Error('Argument out of bounds'); }, - out: '', }, - { - name: '1 non-empty line', - in: { - lineCount: 1, - lineAt(): { text: string } { - return { text: 'hi' }; - }, + out: '', + }, + { + name: '1 non-empty line', + in: { + lineCount: 1, + lineAt(): { text: string } { + return { text: 'hi' }; }, - out: 'hi', }, - { - name: '2 non-empty lines', - in: { - lineCount: 2, - lineAt(i: number): { text: string } { - const result = !i ? 'hello' : 'world'; - return { text: result }; - }, + out: 'hi', + }, + { + name: '2 non-empty lines', + in: { + lineCount: 2, + lineAt(i: number): { text: string } { + const result = !i ? 'hello' : 'world'; + return { text: result }; }, - out: 'hello\nworld', }, - { - name: '3 non-empty lines', - in: { - lineCount: 3, - lineAt(i: 0 | 1 | 2): { text: string } { - const map: Record<0 | 1 | 2, string> = { - [0]: 'how', - [1]: 'are', - [2]: 'you', - }; - const result = map[i]; - return { text: result }; - }, + out: 'hello\nworld', + }, + { + name: '3 non-empty lines', + in: { + lineCount: 3, + lineAt(i: 0 | 1 | 2): { text: string } { + const map: Record<0 | 1 | 2, string> = { + [0]: 'how', + [1]: 'are', + [2]: 'you', + }; + const result = map[i]; + return { text: result }; }, - out: 'how\nare\nyou', }, - { - name: '1 empty line, nothing else', - in: { - lineCount: 1, - lineAt(): { text: string } { - return { text: '' }; - }, + out: 'how\nare\nyou', + }, + { + name: '1 empty line, nothing else', + in: { + lineCount: 1, + lineAt(): { text: string } { + return { text: '' }; }, - out: '', }, - { - name: '2 empty lines, nothing else', - in: { - lineCount: 2, - lineAt(): { text: string } { - return { text: '' }; - }, + out: '', + }, + { + name: '2 empty lines, nothing else', + in: { + lineCount: 2, + lineAt(): { text: string } { + return { text: '' }; }, - out: '\n', }, - ]; + out: '\n', + }, + ]; - myTests.forEach((myTest) => - test(myTest.name, () => { - assert.strictEqual(documentToString(myTest.in), myTest.out); - }), - ); + myTests.forEach((myTest) => + test(myTest.name, () => { + assert.strictEqual(documentToString(myTest.in), myTest.out); + }), + ); +}); + +interface FormatTest { + /** Name of the file, excluding the suffix (@see inFilenameSuffix, @see outFilenameSuffix) */ + filenameRoot: string; + // Any properties not provided will use `defaultOptions` below + options?: Partial; +} + +suite.only('internalFormat', () => { + // Currently in `out` folder, need to get back to main `src` folder + const filesParentPath = path.join( + __dirname, // ./out/src/providers + '..', // ./out/src + '..', // ./out + '..', // . + 'src', // ./src + 'providers', // ./src/providers + 'samples', // ./src/providers/samples + ); + + const inFilenameSuffix = '.in.ahk'; + const outFilenameSuffix = '.out.ahk'; + + const fileToString = (path: string): string => + fs.readFileSync(path).toString(); + + /** Default formatting options, meant to match default extension settings */ + const defaultOptions = { + tabSize: 4, + insertSpaces: true, + allowedNumberOfEmptyLines: 1, + indentCodeAfterLabel: true, + indentCodeAfterIfDirective: true, + preserveIndent: false, + trimExtraSpaces: false, + }; + + const formatTests: FormatTest[] = [ + { filenameRoot: '25-multiline-string' }, + { filenameRoot: '28-switch-case' }, + { filenameRoot: '40-command-inside-text' }, + { filenameRoot: '55-if-directive' }, + { filenameRoot: '56-return-command-after-label' }, + { filenameRoot: '58-parentheses-indentation' }, + { filenameRoot: '59-one-command-indentation' }, + { filenameRoot: '72-paren-hotkey' }, + { filenameRoot: '119-semicolon-inside-string' }, + { filenameRoot: '161-colon-on-last-position' }, + { filenameRoot: '180-if-else-braces' }, + { + filenameRoot: '182-multiple-newlines', + options: { allowedNumberOfEmptyLines: 2 }, + }, + { filenameRoot: '184-continuation-section-expression' }, + { filenameRoot: '184-continuation-section-object' }, + { filenameRoot: '184-continuation-section-text' }, + { filenameRoot: '185-block-comment' }, + { + filenameRoot: '187-comments-at-end-of-line', + options: { trimExtraSpaces: false }, + }, + { filenameRoot: '188-one-command-code-in-text' }, + { + filenameRoot: '189-space-at-end-of-line', + options: { trimExtraSpaces: true }, + }, + { + filenameRoot: '192-preserve-indent-true', + options: { preserveIndent: true }, + }, + { filenameRoot: '255-close-brace' }, + { filenameRoot: '255-else-if' }, + { filenameRoot: '255-if-loop-mix' }, + { filenameRoot: '255-return-function' }, + { filenameRoot: '255-return-label' }, + { filenameRoot: '255-style-allman' }, + { filenameRoot: '255-style-k-and-r' }, + { filenameRoot: '255-style-mix' }, + { filenameRoot: '255-style-one-true-brace' }, + { filenameRoot: '290-ifmsgbox' }, + { filenameRoot: '291-single-line-comment' }, + { filenameRoot: '316-if-object-continuation-section' }, + //* 411 should pass with default settings! + { filenameRoot: '411-extra-spaces-in-string' }, + { filenameRoot: '429-single-line-hotkey' }, + { filenameRoot: '432-label-inside-code-block' }, + { + filenameRoot: + '440-fall-through-single-line-hotkey-with-if-directive', + }, + { filenameRoot: '442-fall-through-single-line-hotkey-with-function' }, + { filenameRoot: 'ahk-explorer' }, + { filenameRoot: 'align-assignment' }, + { filenameRoot: 'demo' }, + { + filenameRoot: 'indent-code-after-if-directive-false', + options: { indentCodeAfterIfDirective: false }, + }, + { + filenameRoot: 'indent-code-after-if-directive-true', + options: { indentCodeAfterIfDirective: true }, + }, + { + filenameRoot: 'indent-code-after-label-false', + options: { indentCodeAfterLabel: false }, + }, + { + filenameRoot: 'indent-code-after-label-true', + options: { indentCodeAfterLabel: true }, + }, + { + filenameRoot: 'insert-spaces-false', + options: { insertSpaces: false }, + }, + { filenameRoot: 'legacy-text-if-directive' }, + { filenameRoot: 'label-colon' }, + { filenameRoot: 'label-combination' }, + { filenameRoot: 'label-fall-through' }, + { filenameRoot: 'label-specific-name' }, + { filenameRoot: 'return-exit-exitapp' }, + { filenameRoot: 'single-line-comment' }, + { + filenameRoot: 'tab-size-2', + options: { tabSize: 2 }, + }, + ]; + + formatTests.forEach((formatTest) => { + test(`${formatTest.filenameRoot}`, async () => { + // Arrange + const inFilePath = path.join( + filesParentPath, + `${formatTest.filenameRoot}${inFilenameSuffix}`, + ); + const inFileString = fileToString(inFilePath); + const outFilePath = path.join( + filesParentPath, + `${formatTest.filenameRoot}${outFilenameSuffix}`, + ); + const outFileString = fileToString(outFilePath); + const options = { ...defaultOptions, ...formatTest.options }; + + // Act + const actual = internalFormat(inFileString, options); + + // Assert + assert.strictEqual(actual, outFileString); + }); }); }); diff --git a/src/providers/formattingProvider.utils.ts b/src/providers/formattingProvider.utils.ts index 4d542bde..512aefd8 100644 --- a/src/providers/formattingProvider.utils.ts +++ b/src/providers/formattingProvider.utils.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { commentRegExp } from '../common/constants'; import { FormatOptions } from './formattingProvider.types'; +import { isDeepStrictEqual } from 'util'; /** Stringify a document, using consistent `\n` line separators */ export const documentToString = (document: { @@ -679,3 +680,946 @@ export function calculateDepth( const charsNum = indentationChars?.[0].length ?? 0; return options.insertSpaces ? charsNum / options.tabSize : charsNum; } + +export const internalFormat = ( + stringToFormat: string, + options: FormatOptions, +): string => { + let formattedString = ''; + + // INDENTATION + /** Current level of indentation. 0 = top-level, no indentation. */ + let depth = 0; + /** Level of indentation on previous line */ + let prevLineDepth = 0; + /** + * It's marker for `Return`, `ExitApp`, `Hotkeys` and `Labels`, + * which allow/disallow for them to be un-indented. + * + * ------------------------------------------------------------------------- + * `tagDepth === 0`: + * + * Indentation level was decreased by `Return`, `ExitApp`, `#If Directive`, + * so they placed on same indentation level as `Label`. + * + * Decrement of indentation level by `Label` is disallowed (previous + * `Label` finished with `Return` or `ExitApp` command and un-indent for + * fall-through scenario not needed). + * + * ------------------------------------------------------------------------- + * `tagDepth === depth`: + * + * Current indentation level is in sync with `Label` indentation level + * (no additional indent added by block `{}`, `oneCommandCode`, etc...). + * + * `Return`, `ExitApp`, `Fall-Through Label` allowed to be un-indented, + * so they will be placed on same indentation level as `Label`. + * + * `Label` allowed to be un-indented for fall-through scenario. + * + * ------------------------------------------------------------------------- + * `tagDepth !== depth`: + * + * `Return`, `ExitApp`, `Label` disallowed to be un-indented, so they + * will obey indentation rules as code above them (`Return` inside + * function, block `{}`, `oneCommandCode`, etc... stay on same + * indentation level as code above them). + * + * ------------------------------------------------------------------------- + * `tagDepth > 0` : + * + * `#If Directive` allowed to be un-indented by `tagDepth` value (jump + * several indentation levels). + * + * ------------------------------------------------------------------------- + * `tagDepth = depth`: + * + * Only `Label` makes syncing `tagDepth` with `depth`. + * + * `Case:` and `Default:` must not make syncing to disallow `Return`, + * `ExitApp` and `Label` to un-indent inside `Switch-Case` block. + * + * ------------------------------------------------------------------------- + * `tagDepth = 0`: + * + * `Return`, `ExitApp`, `#If Directive`, `HotkeySingleLine` resets + * `tagDepth` value, when they un-indented. + */ + let tagDepth = 0; + + // FLOW OF CONTROL and IF-ELSE nesting + /** + * `True` if this line is an one-statement block. Example: + * ```ahk + * if (var) ; false + * MsgBox ; true + * SoundBeep ; false + * ``` + */ + let oneCommandCode = false; + /** Previous line is one command code */ + let prevLineIsOneCommandCode = false; + /** + * Detect or not detect `oneCommandCode`. + * Every iteration it's `true`, but becomes `false` when formatter increase + * indent for next line by open brace `{`. + * It's prevent wrong extra indent, when `{` present after flow of control + * statement: one indent for `{` and additional indent for `oneCommandCode`. + */ + let detectOneCommandCode = true; + /** + * Object with array of indent level of `if` not completed by `else`. + * + * Allow us to de-indent (jump several indentation levels) `else` to last + * not complete `if` and de-indent (jump several indentation levels) code + * that exit nested flow of control statements inside block of code `{}`. + * + * Every time we meet `{` we `push` delimiter `-1` to array. + * + * Every time we meet `}` we delete last delimiter `-1` from array and all + * elements after it. + * + * Every time we meet `if` we `push` current `depth` to array. + * + * Every time we meet `else` we `pop` element from array. + * + * When code leaves `if` nesting we `splice` (delete) element(s) in array + * after last delimiter. + * + * Example: + * ```ahk + * ; [-1] + * if (var) ; [-1, 0] + * { ; [-1, 0, -1] + * if (var) ; [-1, 0, -1, 1] + * if (var) ; [-1, 0, -1, 1, 2] + * code + * else ; [-1, 0, -1, 1], de-indent to last not complete IF, + * code ; complete it by deleting last element + * code ; [-1, 0, -1] de-indent to first not complete IF inside + * } ; [-1, 0] code block + * code ; [-1] + * ``` + */ + let ifDepth = new FlowOfControlNestDepth(); + /** + * Object with array of indent level of first flow of control statement + * without open brace `{` with nested code inside it. + * + * Allow us to de-indent (jump several indentation levels) code + * that exit nested flow of control statements inside block of code `{}`. + * + * Every time we meet `{` we check did we add by mistake `depth` of last + * flow of control, if yes - revert changes, `push` delimiter `-1` to array. + * + * Every time we meet `}` we delete last delimiter `-1` and all elements + * after it. + * + * Every time we meet flow of control statement without `{` we `push` + * current `depth` to array (only if last element is delimiter). + * + * When code leaves flow of control nesting we `splice` (delete) element(s) + * in array after last delimiter. + * + * Example: + * ```ahk + * ; [-1] + * loop ; [-1, 0] added by mistake + * { ; [-1, -1] revert changes and add delimiter + * loop ; [-1, -1, 1] + * loop ; [-1, -1, 1] not added + * code + * code ; [-1, -1] de-indent to first flow of control inside + * } ; [-1] code block + * code ; [-1] + * ``` + */ + let focDepth = new FlowOfControlNestDepth(); + + // ALIGN ASSIGNMENT + /** + * Formatter's directive: + * ```ahk + * ;@AHK++AlignAssignmentOn + * ;@AHK++AlignAssignmentOff + * ``` + * Align assignment between this directives + */ + let alignAssignment = false; + /** Code block with assignment to be aligned */ + let assignmentBlock: string[] = []; + + // CONTINUATION SECTION + /** + * Continuation section: Expression, Object + * ```ahk + * obj := { a: 1 ; false + * , b: 2 } ; true + * if a = 1 ; false + * and b = 2 ; true + * ``` + */ + let continuationSectionExpression = false; + /** + * True iff continuation section is for text and should be formatted + * ```ahk + * ( LTrim + * Indented line of text + * ) + * ``` + */ + let continuationSectionTextFormat = false; + /** + * True iff continuation section is for text but should **not** be formatted + * ```ahk + * ( [NO LTrim option!] + * Line of text with preserved user formatting + * ) + * ``` + */ + let continuationSectionTextNotFormat = false; + /** + * Level of indentation of current line increased by open brace `{`, but not + * inside expression continuation section. + */ + let openBraceIndent = false; + /** + * The indentation of `oneCommandCode` is delayed, because the current line + * is an expression continuation section. The indentation is delayed by + * temporarily disabling `oneCommandCode`. + */ + let deferredOneCommandCode = false; + /** + * Indent level of open brace `{` that belongs to object's initialization + * with continuation section. + */ + let openBraceObjectDepth = -1; + + // BLOCK COMMENT + /** This line is block comment */ + let blockComment = false; + /** Base indent, that block comment had in original code */ + let blockCommentIndent = ''; + /** + * Formatter's directive: + * ```ahk + * ;@AHK++FormatBlockCommentOn + * ;@AHK++FormatBlockCommentOff + * ``` + * Format text inside block comment like regular code + */ + let formatBlockComment = false; + // Save formatter state to this variables on enter of block comment and + // restore them on exit of block comment + let preBlockCommentDepth = 0; + let preBlockCommentTagDepth = 0; + let preBlockCommentPrevLineDepth = 0; + let preBlockCommentOneCommandCode = false; + let preBlockCommentIfDepth = new FlowOfControlNestDepth(); + let preBlockCommentFocDepth = new FlowOfControlNestDepth(); + + // SETTINGS' ALIASES + const indentCodeAfterLabel = options.indentCodeAfterLabel; + const indentCodeAfterIfDirective = options.indentCodeAfterIfDirective; + const trimSpaces = options.trimExtraSpaces; + + // REGULAR EXPRESSION + /** Formatter's directive `;@AHK++AlignAssignmentOn` */ + const ahkAlignAssignmentOn = /;\s*@AHK\+\+AlignAssignmentOn/i; + /** Formatter's directive `;@AHK++AlignAssignmentOff` */ + const ahkAlignAssignmentOff = /;\s*@AHK\+\+AlignAssignmentOff/i; + /** Formatter's directive `;@AHK++FormatBlockCommentOn` */ + const ahkFormatBlockCommentOn = /;\s*@AHK\+\+FormatBlockCommentOn/i; + /** Formatter's directive `;@AHK++FormatBlockCommentOff` */ + const ahkFormatBlockCommentOff = /;\s*@AHK\+\+FormatBlockCommentOff/i; + /** + * A line that starts with `and`, `or`, `||`, `&&`, a comma, or a period is + * automatically merged with the line directly above it (the same is true + * for all other expression operators except `++` and `--`). + * + * Skip `++`, `--`, block comments `/ *` and `* /` + */ + const continuationSection = + /^(((and|or|not)\b)|[\^!~?:&<>=.,|]|\+(?!\+)|-(?!-)|\/(?!\*)|\*(?!\/))/; + /** + * Label name may consist of any characters other than `space`, `tab`, + * `comma` and the escape character (`). Not ended by double colon `::`. + * + * Generally, aside from whitespace and comments, + * no other code can be written on the same line as a label. + * + * Example: `Label:` + */ + const label = /^[^\s\t,`]+(? { + const purifiedLine = purify(originalLine).toLowerCase(); + /** The line comment. Empty string if no line comment exists */ + const comment = commentRegExp.exec(originalLine)?.[0] ?? ''; + let formattedLine = originalLine.replace(commentRegExp, ''); // Remove single line comment + formattedLine = trimExtraSpaces(formattedLine, trimSpaces) // Remove extra spaces between words + .concat(comment) // Add removed single line comment back + .trim(); + /** Line is empty or this is a single line comment */ + const emptyLine = purifiedLine === ''; + + detectOneCommandCode = true; + + const openBraceNum = braceNumber(purifiedLine, '{'); + const closeBraceNum = braceNumber(purifiedLine, '}'); + + // ===================================================================== + // | THIS LINE | + // ===================================================================== + + // STOP DIRECTIVE for formatter + if (emptyLine) { + if (alignAssignment && comment.match(ahkAlignAssignmentOff)) { + alignAssignment = false; + assignmentBlock = alignTextAssignOperator(assignmentBlock); + // Save aligned block + assignmentBlock.forEach((alignedFormattedLine, index) => { + formattedString += buildIndentedLine( + // restore 'lineIndex' before 'assignmentBlock' and add + // 'index + 1' + lineIndex - assignmentBlock.length + index + 1, + lines.length, + alignedFormattedLine, + depth, + options, + ); + }); + assignmentBlock = []; + } + if (formatBlockComment && comment.match(ahkFormatBlockCommentOff)) { + formatBlockComment = false; + } + } + + // ALIGN ASSIGNMENT + if (alignAssignment) { + assignmentBlock.push(formattedLine); + if (lineIndex !== lines.length - 1) { + // skip to the next iteration + return; + } + // Save aligned block if we reach end of text, but didn't find stop + // directive ';@AHK++AlignAssignmentOff' + assignmentBlock.forEach((alignedFormattedLine, index) => { + formattedString += buildIndentedLine( + // restore 'lineIndex' before 'assignmentBlock' and add + // 'index + 1' + lineIndex - assignmentBlock.length + index + 1, + lines.length, + alignedFormattedLine, + depth, + options, + ); + }); + assignmentBlock = []; + } + + // BLOCK COMMENT + // The /* and */ symbols can be used to comment out an entire section, + // but only if the symbols appear at the beginning of a line (excluding + // whitespace), like in this example: + // /* + // MsgBox, This line is commented out (disabled). + // MsgBox, Common mistake: */ this does not end the comment. + // MsgBox, This line is commented out. + // */ + if (!blockComment && originalLine.match(/^\s*\/\*/)) { + // found start '/*' pattern + blockComment = true; + // Save first capture group (original indent) + blockCommentIndent = originalLine.match(/(^\s*)\/\*/)?.[1]; + if (formatBlockComment) { + // save indent values on block comment enter + preBlockCommentDepth = depth; + preBlockCommentTagDepth = tagDepth; + preBlockCommentPrevLineDepth = prevLineDepth; + preBlockCommentOneCommandCode = oneCommandCode; + preBlockCommentIfDepth = ifDepth; + preBlockCommentFocDepth = focDepth; + // reset indent values to default values + tagDepth = depth; + prevLineDepth = depth; + oneCommandCode = false; + ifDepth = new FlowOfControlNestDepth(); + focDepth = new FlowOfControlNestDepth(); + } + } + + // BLOCK COMMENT + if (blockComment) { + // Save block comment line only if user don't want format it content + if (!formatBlockComment) { + let blockCommentLine = ''; + if (originalLine.startsWith(blockCommentIndent)) { + blockCommentLine = originalLine.substring( + blockCommentIndent.length, + ); + } else { + blockCommentLine = originalLine; + } + formattedString += buildIndentedLine( + lineIndex, + lines.length, + blockCommentLine.trimEnd(), + depth, + options, + ); + } + if (originalLine.match(/^\s*\*\//)) { + // found end '*/' pattern + blockComment = false; + if (formatBlockComment) { + // restore indent values on block comment exit + depth = preBlockCommentDepth; + tagDepth = preBlockCommentTagDepth; + prevLineDepth = preBlockCommentPrevLineDepth; + oneCommandCode = preBlockCommentOneCommandCode; + ifDepth = preBlockCommentIfDepth; + focDepth = preBlockCommentFocDepth; + } + } + if (!formatBlockComment) { + return; + } + } + + // SINGLE LINE COMMENT + if ( + emptyLine && + // skip formatter's directives + !comment.match(ahkAlignAssignmentOn) && + !comment.match(ahkAlignAssignmentOff) && + !comment.match(ahkFormatBlockCommentOn) && + !comment.match(ahkFormatBlockCommentOff) + ) { + // save with zero indent (indent value don't matter here) + formattedString += buildIndentedLine( + lineIndex, + lines.length, + formattedLine, + 0, + options, + ); + return; + } + + // CONTINUATION SECTION: Text [Not Formatted] Start + // ( [NO LTrim option!] <-- check this START parenthesis + // Line of text with preserved user formatting + // ) + // Skip hotkey: (:: + if (purifiedLine.match(/^\((?!::)(?!.*\bltrim\b)/)) { + continuationSectionTextNotFormat = true; + } + + // CONTINUATION SECTION: Text [Not Formatted] Save with original indent + if (continuationSectionTextNotFormat) { + formattedString += originalLine.trimEnd() + '\n'; + // CONTINUATION SECTION: Text [Not Formatted] Stop + // ( [NO LTrim option!] + // Line of text with preserved user formatting + // ) <-- check this STOP parenthesis + if (purifiedLine.match(/^\)/)) { + continuationSectionTextNotFormat = false; + } + return; + } + + // CONTINUATION SECTION: Text [Formatted] Stop + // ( LTrim + // Line of text + // ) <-- check this STOP parenthesis + if (continuationSectionTextFormat && purifiedLine.match(/^\)/)) { + continuationSectionTextFormat = false; + depth--; + } + + // CONTINUATION SECTION: Text [Formatted] Save indented + if (continuationSectionTextFormat) { + formattedString += buildIndentedLine( + lineIndex, + lines.length, + originalLine.trim(), + depth, + options, + ); + return; + } + + // CONTINUATION SECTION: Expression, Object, Flow of Control nesting + // obj := { a: 1 + // , b: 2 } + // if a = 1 + // and b = 2 + if ( + purifiedLine.match(continuationSection) && + // skip Hotkeys:: and ::Hotstrings:: (they has '::') + !purifiedLine.match(/::/) + ) { + continuationSectionExpression = true; + // CONTINUATION SECTION: Object + // obj := { a: 1 + // , b: 2 <-- revert one! indent level after open brace or + // , c: 3 } multiply open braces + if (openBraceIndent) { + depth--; + openBraceObjectDepth = prevLineDepth; + } + // CONTINUATION SECTION: Expression + // if a = 1 + // or b = 2 <-- revert indent for oneCommandCode and make it + // MsgBox deferred + if (oneCommandCode) { + deferredOneCommandCode = true; + oneCommandCode = false; + prevLineIsOneCommandCode = false; + depth--; + } + // CONTINUATION SECTION: Flow of Control nesting + // Loop + // code ; previous line is one command code + // , code <-- restore oneCommandCode depth + // code + if (prevLineIsOneCommandCode) { + oneCommandCode = true; + depth++; + } + depth++; + } + + // CONTINUATION SECTION: Expression - Deferred oneCommandCode indent + // if a = 1 + // or b = 2 + // MsgBox <-- restore deferred oneCommandCode + if (deferredOneCommandCode && !continuationSectionExpression) { + deferredOneCommandCode = false; + oneCommandCode = true; + depth++; + } + + // CLOSE BRACE + if (closeBraceNum) { + // FLOW OF CONTROL + // Example (restore close brace depth): + // foo() { + // for + // if + // return + // } ; <-- de-indent from all nesting before loosing information + // about depth via focDepth.exitBlockOfCode() below + if (focDepth.last() > -1) { + depth = focDepth.last(); + } + ifDepth.exitBlockOfCode(closeBraceNum); + focDepth.exitBlockOfCode(closeBraceNum); + // CONTINUATION SECTION: Object + // obj := { a: 1 + // , b: 2 + // , c: 3 } <-- skip de-indent by brace in Continuation Section: Object + if (!continuationSectionExpression) { + depth -= closeBraceNum; + } + } + + // OPEN BRACE + if (openBraceNum) { + // ONE COMMAND CODE + // else + // Loop { <-- skip de-indent one command code with open brace + // code + if ( + (oneCommandCode || deferredOneCommandCode) && + !nextLineIsOneCommandCode(purifiedLine) + ) { + if (deferredOneCommandCode) { + // if (a = 4 + // and b = 5) { + // MsgBox <-- disable deferredOneCommandCode indent + // } + deferredOneCommandCode = false; + } else if (purifiedLine.match(/^{/)) { + // if (var) + // { <-- revert oneCommandCode indent for open brace + // MsgBox + // } + // if (var) + // obj := { key1: val1 <-- but not for object continuation + // , key2: val2 } section + oneCommandCode = false; + depth -= openBraceNum; + } + // FLOW OF CONTROL revert added by mistake + // Loop, %var% + // { <-- check open brace below flow of control statement + // code + // } + if (depth === focDepth.last()) { + focDepth.pop(); + } + } + } + + // FLOW OF CONTROL de-indent from all nesting + // if (a > 0 + // and b > 0) <-- skip continuation section + // code <-- skip one command code + // /* block comment */ <-- skip block comment + // code <-- de-indent + if ( + (ifDepth.last() > -1 || focDepth.last() > -1) && + !continuationSectionExpression && + !oneCommandCode && + (!blockComment || formatBlockComment) + ) { + // Else: <-- skip valid LABEL + if (purifiedLine.match(/^}? ?else\b(?!:)/)) { + // { + // if + // if + // loop + // loop + // code + // else <-- de-indent "ELSE" to last not complete "IF" + // code + // else <-- de-indent "ELSE" to last not complete "IF" + // code + // code + // } + depth = ifDepth.pop(); + } else if (!purifiedLine.match(/^{/) && !purifiedLine.match(/^}/)) { + // Example (skip irrelevant braces): + // if <-- relevant + // { <-- skip irrelevant + // code <-- relevant + // } <-- skip irrelevant + // code <-- relevant + // Example (main logic): + // if | loop + // if | loop + // code | code + // code | code <-- de-indent from all nesting + const restoreIfDepth: number | undefined = + ifDepth.restoreDepth(); + const restoreFocDepth: number | undefined = + focDepth.restoreDepth(); + if ( + restoreIfDepth !== undefined && + restoreFocDepth !== undefined + ) { + depth = Math.min(restoreIfDepth, restoreFocDepth); + } else { + depth = restoreIfDepth ?? restoreFocDepth; + } + } + } + + // #IF DIRECTIVE + // #IfWinActive WinTitle1 + // Hotkey:: + // #IfWinActive WinTitle2 <-- fall-through scenario for #IF DIRECTIVE + // Hotkey:: with parameters + // #If <-- de-indent #IF DIRECTIVE without parameters + if (purifiedLine.match('^' + sharpDirective + '\\b')) { + if (tagDepth > 0) { + depth -= tagDepth; + } else { + depth--; + } + } + + // Return, Exit, ExitApp + // Label: + // code + // Return <-- force de-indent by one level for labels + if ( + purifiedLine.match(/^(return|exit|exitapp)\b/) && + tagDepth === depth + ) { + tagDepth = 0; + depth--; + } + + // SWITCH-CASE-DEFAULT or LABEL: or HOTKEY:: + if (purifiedLine.match(switchCaseDefault)) { + // Case: or Default: + depth--; + } else if ( + purifiedLine.match(label) || + purifiedLine.match(hotkey) || + purifiedLine.match(hotkeySingleLine) + ) { + if (indentCodeAfterLabel) { + // Label: or Hotkey:: + // De-indent label or hotkey, if they not end with 'return' + // command. + // This is fall-through scenario. Example: + // Label1: <-- de-indent + // code + // Label2: <-- de-indent + // code + // return + // De-indent single-line hotkey, after label. + // This is fall-through scenario. Example: + // F1:: + // F2:: <-- de-indent + // F3:: foo() <-- de-indent + if (tagDepth === depth) { + depth--; + } + } + } + + // De-indent by label may produce negative 'depth', it's normal behavior + if (depth < 0) { + depth = 0; + } + if (preBlockCommentDepth < 0) { + preBlockCommentDepth = 0; + } + + prevLineDepth = depth; + + // Save indented line + formattedString += buildIndentedLine( + lineIndex, + lines.length, + formattedLine, + depth, + options, + ); + + // ===================================================================== + // | NEXT LINE | + // ===================================================================== + + // START DIRECTIVE for formatter + if (emptyLine) { + if (comment.match(ahkAlignAssignmentOn)) { + alignAssignment = true; + } else if (comment.match(ahkFormatBlockCommentOn)) { + formatBlockComment = true; + } + } + + // ONE COMMAND CODE + if ( + oneCommandCode && + // Don't change indentation on block comment after one command code. + // Change indentation inside block comment, if user wants to format + // block comment. + (!blockComment || formatBlockComment) + ) { + oneCommandCode = false; + prevLineIsOneCommandCode = true; + // FLOW OF CONTROL + // if (var) + // if (var) <-- don't de-indent nested flow of control statement + if (!nextLineIsOneCommandCode(purifiedLine)) { + depth--; + } + } else { + prevLineIsOneCommandCode = false; + } + + // FLOW OF CONTROL + // Loop, %var% <-- flow of control statement without open brace + // code + // code + if ( + nextLineIsOneCommandCode(purifiedLine) && + openBraceNum === 0 && + focDepth.last() === -1 + ) { + focDepth.push(depth); + } + + // IF-ELSE complete tracking + // if { <-- check IF + // code + // } else if { <-- check IF + // code + // } else if <-- check IF + // code + // else if <-- check IF + // code + // If: <-- skip valid LABEL + if (purifiedLine.match(/^(}? ?else )?if\b(?!:)/)) { + ifDepth.push(depth); + } + + // OPEN BRACE + if (openBraceNum) { + depth += openBraceNum; + // Do not detect 'oneCommandCode', because it will produce extra + // indent for next line like in example below: + // if { + // code <-- wrong extra indent by oneCommandCode + // code + // } + detectOneCommandCode = false; + // CONTINUATION SECTION: Nested Objects + if (!continuationSectionExpression) { + openBraceIndent = true; + } else { + openBraceIndent = false; + } + // FLOW OF CONTROL + ifDepth.enterBlockOfCode(openBraceNum); + focDepth.enterBlockOfCode(openBraceNum); + } else { + openBraceIndent = false; + } + + // #IF DIRECTIVE with parameters + // #If Expression <-- indent next line after '#IF DIRECTIVE' + // F1:: MsgBox Help + if ( + purifiedLine.match('^' + sharpDirective + '\\b.+') && + indentCodeAfterIfDirective + ) { + depth++; + tagDepth = 0; + } + + // SWITCH-CASE-DEFAULT or LABEL: or HOTKEY:: + if (purifiedLine.match(switchCaseDefault)) { + // Case: or Default: <-- indent next line + // code + depth++; + // Do not sync here 'tagDepth' with 'depth' to prevent 'Return' and + // 'ExitApp' to de-indent inside 'Switch-Case-Default' construction! + } else if (purifiedLine.match(label) || purifiedLine.match(hotkey)) { + if (indentCodeAfterLabel) { + // Label: or Hotkey:: <-- indent next line + // code + // Do this only if the LABEL is not inside a nested code + if (focDepth.depth.length === 1) { + depth++; + tagDepth = depth; + } + } + } else if (purifiedLine.match(hotkeySingleLine)) { + tagDepth = 0; + } + + // CONTINUATION SECTION: Expression, Object + if (continuationSectionExpression) { + continuationSectionExpression = false; + // Object - Check close braces of nested objects + // obj := { a: 1 + // , b : { c: 2 + // , d: 3 } } <-- multiply close braces in nested objects + if (closeBraceNum) { + depth -= closeBraceNum; + // obj := { a: 1 + // , b : { c: 2 + // , d: 3 } } <-- revert indent after last close brace + if (openBraceObjectDepth === depth) { + openBraceObjectDepth = -1; + depth++; + } + } + // Expression - De-indent next line + // isPositive := x > 0 + // and y > 0 <-- de-indent next line after continuation section + // x++ + depth--; + } + + // CONTINUATION SECTION: Text [Formatted] Start + // ( LTrim <-- check this START parenthesis + // Indented line of text + // ) + // Skip hotkey "open parenthesis" (:: + if (purifiedLine.match(/^\((?!::)(?=.*\bltrim\b)/)) { + continuationSectionTextFormat = true; + depth++; + } + + // ONE COMMAND CODE + // Loop, %var% <-- indent next line + // code + // code + if (detectOneCommandCode && nextLineIsOneCommandCode(purifiedLine)) { + oneCommandCode = true; + depth++; + } + + // DEBUG CONSOLE OUTPUT + if (lineIndex === lines.length - 1) { + if ( + !( + isDeepStrictEqual(ifDepth.depth, [-1]) || + isDeepStrictEqual(ifDepth.depth, [-1, 0]) + ) && + !( + isDeepStrictEqual(focDepth.depth, [-1]) || + isDeepStrictEqual(focDepth.depth, [-1, 0]) + ) + ) { + // If code is finished (number of open and close braces are + // equal, flow of control statements has code after one command + // code, etc...) arrays must be equal [-1] or [-1, 0]. Last zero + // in array stays, because formatter waits code after close + // brace, but instead reaches EOF. If not equal, syntax is + // incorrect of there is bug in formatter logic. + console.error( + [ + 'Internal formatter data:', + 'ifDepth:', + ifDepth.depth, + 'focDepth:', + focDepth.depth, + ].join('\n'), + ); + } + } + }); + + formattedString = alignSingleLineComments(formattedString, options); + + formattedString = removeEmptyLines( + formattedString, + options.allowedNumberOfEmptyLines, + ); + + return formattedString; +}; diff --git a/src/providers/samples/411-extra-spaces-in-string.in.ahk b/src/providers/samples/411-extra-spaces-in-string.in.ahk new file mode 100644 index 00000000..9e894f81 --- /dev/null +++ b/src/providers/samples/411-extra-spaces-in-string.in.ahk @@ -0,0 +1,3 @@ +; https://github.com/mark-wiemer-org/ahkpp/issues/411 +string_with_multiple_spaces := " " +string_with_multiple_spaces_2 := " ahk is nice " diff --git a/src/providers/samples/411-extra-spaces-in-string.out.ahk b/src/providers/samples/411-extra-spaces-in-string.out.ahk new file mode 100644 index 00000000..9e894f81 --- /dev/null +++ b/src/providers/samples/411-extra-spaces-in-string.out.ahk @@ -0,0 +1,3 @@ +; https://github.com/mark-wiemer-org/ahkpp/issues/411 +string_with_multiple_spaces := " " +string_with_multiple_spaces_2 := " ahk is nice " diff --git a/src/providers/samples/demo.out.ahk b/src/providers/samples/demo.out.ahk index 28c9360e..7b89117f 100644 --- a/src/providers/samples/demo.out.ahk +++ b/src/providers/samples/demo.out.ahk @@ -67,7 +67,7 @@ Collapse me! ; Function calls (with a space before parens) foo() bar () -baz () ; multiple spaces +baz () ; multiple spaces ; SUBROUTINES