diff --git a/src/commands/index.ts b/src/commands/index.ts index 4f5c2b0..f3a74a0 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,5 +1,6 @@ import { hoistRegExp } from './hoist-regexp' import { inlineArrow } from './inline-arrow' +import { keepAligned } from './keep-aligned' import { keepSorted } from './keep-sorted' import { keepUnique } from './keep-unique' import { noShorthand } from './no-shorthand' @@ -21,6 +22,7 @@ import { toTernary } from './to-ternary' export { hoistRegExp, inlineArrow, + keepAligned, keepSorted, keepUnique, noShorthand, @@ -43,6 +45,7 @@ export { export const builtinCommands = [ hoistRegExp, inlineArrow, + keepAligned, keepSorted, keepUnique, noShorthand, diff --git a/src/commands/keep-aligned.md b/src/commands/keep-aligned.md new file mode 100644 index 0000000..0312bc6 --- /dev/null +++ b/src/commands/keep-aligned.md @@ -0,0 +1,65 @@ +# `keep-aligned` + +Keep specific symbols within a block of code are aligned vertically. + +## Triggers + +- `/// keep-aligned ` +- `// @keep-aligned ` + +## Examples + + + +```typescript +// @keep-aligned , , , +export const matrix = [ + 1, 0, 0, + 0.866, -0.5, 0, + 0.5, 0.866, 42, +] +``` + +Will be converted to: + + + +```typescript +// @keep-aligned , , , +export const matrix = [ + 1 , 0 , 0 , + 0.866, -0.5 , 0 , + 0.5 , 0.866, 42, +] +``` + +### Repeat Mode + +For the example above where `,` is the only repeating symbol for alignment, `keep-aligned*` could be used instead to indicate a repeating pattern: + + + +```typescript +// @keep-aligned* , +export const matrix = [ + 1, 0, 0, + 0.866, -0.5, 0, + 0.5, 0.866, 42, +] +``` + +Will produce the same result. + +> [!TIP] +> This rule does not work well with other spacing rules, namely `style/no-multi-spaces, style/comma-spacing, antfu/consistent-list-newline` were disabled for the example above to work. Consider adding `/* eslint-disable */` to specific ESLint rules for lines affected by this command. +> +> ```typescript +> /* eslint-disable style/no-multi-spaces, style/comma-spacing, antfu/consistent-list-newline */ +> // @keep-aligned , , , +> export const matrix = [ +> 1 , 0 , 0 , +> 0.866, -0.5 , 0 , +> 0.5 , 0.866, 42, +> ] +> /* eslint-enable style/no-multi-spaces, style/comma-spacing, antfu/consistent-list-newline */ +> ``` diff --git a/src/commands/keep-aligned.test.ts b/src/commands/keep-aligned.test.ts new file mode 100644 index 0000000..d471379 --- /dev/null +++ b/src/commands/keep-aligned.test.ts @@ -0,0 +1,86 @@ +import { $, run } from './_test-utils' +import { keepAligned } from './keep-aligned' + +run( + keepAligned, + { + code: $` + // @keep-aligned , , , + export const matrix = [ + 1, 0, 0, + 0.866, -0.5, 0, + 0.5, 0.866, 42, + ] + `, + output(output) { + expect(output).toMatchInlineSnapshot(` + "// @keep-aligned , , , + export const matrix = [ + 1 , 0 , 0 , + 0.866, -0.5 , 0 , + 0.5 , 0.866, 42, + ]" + `) + }, + }, + { + code: $` + // @keep-aligned , , ] + export const matrix = [ + [1, 0, 0], + [0.866, -0.5, 0], + [0.5, 0.866, 42], + ] + `, + output(output) { + expect(output).toMatchInlineSnapshot(` + "// @keep-aligned , , ] + export const matrix = [ + [1 , 0 , 0 ], + [0.866, -0.5 , 0 ], + [0.5 , 0.866, 42], + ] " + `) + }, + }, + { + code: $` + /// keep-aligned* , + export const matrix = [ + 1, 0, 0, 0.866, 0.5, 0.866, 42, + 0.866, -0.5, 0, 0.5, 0.121212, + 0.5, 0.866, 118, 1, 0, 0, 0.866, -0.5, 12, + ] + `, + output(output) { + expect(output).toMatchInlineSnapshot(` + "/// keep-aligned* , + export const matrix = [ + 1 , 0 , 0 , 0.866, 0.5 , 0.866, 42 , + 0.866, -0.5 , 0 , 0.5 , 0.121212, + 0.5 , 0.866, 118, 1 , 0 , 0 , 0.866, -0.5, 12, + ] " + `) + }, + }, + { + code: $` + function foo(arr: number[][], i: number, j: number) { + // @keep-aligned arr[ ] arr[ ] ][j + return arr[i - 1][j - 1] + arr[i - 1][j] + arr[i - 1][j + 1] + + arr[i][j - 1] + arr[i][j] + arr[i][j + 1] + + arr[i + 1][j - 1] + arr[i + 1][j] + arr[i][j + 1] + } + `, + output(output) { + expect(output).toMatchInlineSnapshot(` + "function foo(arr: number[][], i: number, j: number) { + // @keep-aligned arr[ ] arr[ ] ][j + return arr[i - 1][j - 1] + arr[i - 1][j] + arr[i - 1][j + 1] + + arr[i ][j - 1] + arr[i ][j] + arr[i ][j + 1] + + arr[i + 1][j - 1] + arr[i + 1][j] + arr[i ][j + 1] + }" + `) + }, + }, +) diff --git a/src/commands/keep-aligned.ts b/src/commands/keep-aligned.ts new file mode 100644 index 0000000..65d4b3e --- /dev/null +++ b/src/commands/keep-aligned.ts @@ -0,0 +1,68 @@ +import type { Command } from '../types' + +const reLine = /^[/@:]\s*keep-aligned(?\*?)(?(\s+\S+)+)$/ + +export const keepAligned: Command = { + name: 'keep-aligned', + commentType: 'line', + match: comment => comment.value.trim().match(reLine), + action(ctx) { + // this command applies to any node below + const node = ctx.findNodeBelow(() => true) + if (!node) + return + + const alignmentSymbols = ctx.matches.groups?.symbols?.trim().split(/\s+/) + if (!alignmentSymbols) + return ctx.reportError('No alignment symbols provided') + const repeat = ctx.matches.groups?.repeat + + const nLeadingSpaces = node.range[0] - ctx.comment.range[1] - 1 + const text = ctx.context.sourceCode.getText(node, nLeadingSpaces) + const lines = text.split('\n') + const symbolIndices: number[] = [] + + const nSymbols = alignmentSymbols.length + if (nSymbols === 0) + return ctx.reportError('No alignment symbols provided') + + const n = repeat ? Number.MAX_SAFE_INTEGER : nSymbols + let lastPos = 0 + for (let i = 0; i < n && i < 20; i++) { + const symbol = alignmentSymbols[i % nSymbols] + const maxIndex = lines.reduce((maxIndex, line) => + Math.max(line.indexOf(symbol, lastPos), maxIndex), -1) + symbolIndices.push(maxIndex) + + if (maxIndex < 0) { + if (!repeat) + return ctx.reportError(`Alignment symbol "${symbol}" not found`) + else + break + } + + for (let j = 0; j < lines.length; j++) { + const line = lines[j] + const index = line.indexOf(symbol, lastPos) + if (index < 0) + continue + if (index !== maxIndex) { + const padding = maxIndex - index + lines[j] = line.slice(0, index) + ' '.repeat(padding) + line.slice(index) + } + } + lastPos = maxIndex + symbol.length + } + + const modifiedText = lines.join('\n') + if (text === modifiedText) + return + + ctx.report({ + node, + message: 'Keep aligned', + removeComment: false, + fix: fixer => fixer.replaceText(node, modifiedText.slice(nLeadingSpaces)), + }) + }, +}