Skip to content

Commit 45a5e04

Browse files
authored
feat: new command keep-aligned (#32)
1 parent 419052b commit 45a5e04

File tree

4 files changed

+222
-0
lines changed

4 files changed

+222
-0
lines changed

src/commands/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { hoistRegExp } from './hoist-regexp'
22
import { inlineArrow } from './inline-arrow'
3+
import { keepAligned } from './keep-aligned'
34
import { keepSorted } from './keep-sorted'
45
import { keepUnique } from './keep-unique'
56
import { noShorthand } from './no-shorthand'
@@ -21,6 +22,7 @@ import { toTernary } from './to-ternary'
2122
export {
2223
hoistRegExp,
2324
inlineArrow,
25+
keepAligned,
2426
keepSorted,
2527
keepUnique,
2628
noShorthand,
@@ -43,6 +45,7 @@ export {
4345
export const builtinCommands = [
4446
hoistRegExp,
4547
inlineArrow,
48+
keepAligned,
4649
keepSorted,
4750
keepUnique,
4851
noShorthand,

src/commands/keep-aligned.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# `keep-aligned`
2+
3+
Keep specific symbols within a block of code are aligned vertically.
4+
5+
## Triggers
6+
7+
- `/// keep-aligned <symbols>`
8+
- `// @keep-aligned <symbols>`
9+
10+
## Examples
11+
12+
<!-- eslint-skip -->
13+
14+
```typescript
15+
// @keep-aligned , , ,
16+
export const matrix = [
17+
1, 0, 0,
18+
0.866, -0.5, 0,
19+
0.5, 0.866, 42,
20+
]
21+
```
22+
23+
Will be converted to:
24+
25+
<!-- eslint-skip -->
26+
27+
```typescript
28+
// @keep-aligned , , ,
29+
export const matrix = [
30+
1 , 0 , 0 ,
31+
0.866, -0.5 , 0 ,
32+
0.5 , 0.866, 42,
33+
]
34+
```
35+
36+
### Repeat Mode
37+
38+
For the example above where `,` is the only repeating symbol for alignment, `keep-aligned*` could be used instead to indicate a repeating pattern:
39+
40+
<!-- eslint-skip -->
41+
42+
```typescript
43+
// @keep-aligned* ,
44+
export const matrix = [
45+
1, 0, 0,
46+
0.866, -0.5, 0,
47+
0.5, 0.866, 42,
48+
]
49+
```
50+
51+
Will produce the same result.
52+
53+
> [!TIP]
54+
> 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.
55+
>
56+
> ```typescript
57+
> /* eslint-disable style/no-multi-spaces, style/comma-spacing, antfu/consistent-list-newline */
58+
> // @keep-aligned , , ,
59+
> export const matrix = [
60+
> 1 , 0 , 0 ,
61+
> 0.866, -0.5 , 0 ,
62+
> 0.5 , 0.866, 42,
63+
> ]
64+
> /* eslint-enable style/no-multi-spaces, style/comma-spacing, antfu/consistent-list-newline */
65+
> ```

src/commands/keep-aligned.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { $, run } from './_test-utils'
2+
import { keepAligned } from './keep-aligned'
3+
4+
run(
5+
keepAligned,
6+
{
7+
code: $`
8+
// @keep-aligned , , ,
9+
export const matrix = [
10+
1, 0, 0,
11+
0.866, -0.5, 0,
12+
0.5, 0.866, 42,
13+
]
14+
`,
15+
output(output) {
16+
expect(output).toMatchInlineSnapshot(`
17+
"// @keep-aligned , , ,
18+
export const matrix = [
19+
1 , 0 , 0 ,
20+
0.866, -0.5 , 0 ,
21+
0.5 , 0.866, 42,
22+
]"
23+
`)
24+
},
25+
},
26+
{
27+
code: $`
28+
// @keep-aligned , , ]
29+
export const matrix = [
30+
[1, 0, 0],
31+
[0.866, -0.5, 0],
32+
[0.5, 0.866, 42],
33+
]
34+
`,
35+
output(output) {
36+
expect(output).toMatchInlineSnapshot(`
37+
"// @keep-aligned , , ]
38+
export const matrix = [
39+
[1 , 0 , 0 ],
40+
[0.866, -0.5 , 0 ],
41+
[0.5 , 0.866, 42],
42+
] "
43+
`)
44+
},
45+
},
46+
{
47+
code: $`
48+
/// keep-aligned* ,
49+
export const matrix = [
50+
1, 0, 0, 0.866, 0.5, 0.866, 42,
51+
0.866, -0.5, 0, 0.5, 0.121212,
52+
0.5, 0.866, 118, 1, 0, 0, 0.866, -0.5, 12,
53+
]
54+
`,
55+
output(output) {
56+
expect(output).toMatchInlineSnapshot(`
57+
"/// keep-aligned* ,
58+
export const matrix = [
59+
1 , 0 , 0 , 0.866, 0.5 , 0.866, 42 ,
60+
0.866, -0.5 , 0 , 0.5 , 0.121212,
61+
0.5 , 0.866, 118, 1 , 0 , 0 , 0.866, -0.5, 12,
62+
] "
63+
`)
64+
},
65+
},
66+
{
67+
code: $`
68+
function foo(arr: number[][], i: number, j: number) {
69+
// @keep-aligned arr[ ] arr[ ] ][j
70+
return arr[i - 1][j - 1] + arr[i - 1][j] + arr[i - 1][j + 1]
71+
+ arr[i][j - 1] + arr[i][j] + arr[i][j + 1]
72+
+ arr[i + 1][j - 1] + arr[i + 1][j] + arr[i][j + 1]
73+
}
74+
`,
75+
output(output) {
76+
expect(output).toMatchInlineSnapshot(`
77+
"function foo(arr: number[][], i: number, j: number) {
78+
// @keep-aligned arr[ ] arr[ ] ][j
79+
return arr[i - 1][j - 1] + arr[i - 1][j] + arr[i - 1][j + 1]
80+
+ arr[i ][j - 1] + arr[i ][j] + arr[i ][j + 1]
81+
+ arr[i + 1][j - 1] + arr[i + 1][j] + arr[i ][j + 1]
82+
}"
83+
`)
84+
},
85+
},
86+
)

src/commands/keep-aligned.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { Command } from '../types'
2+
3+
const reLine = /^[/@:]\s*keep-aligned(?<repeat>\*?)(?<symbols>(\s+\S+)+)$/
4+
5+
export const keepAligned: Command = {
6+
name: 'keep-aligned',
7+
commentType: 'line',
8+
match: comment => comment.value.trim().match(reLine),
9+
action(ctx) {
10+
// this command applies to any node below
11+
const node = ctx.findNodeBelow(() => true)
12+
if (!node)
13+
return
14+
15+
const alignmentSymbols = ctx.matches.groups?.symbols?.trim().split(/\s+/)
16+
if (!alignmentSymbols)
17+
return ctx.reportError('No alignment symbols provided')
18+
const repeat = ctx.matches.groups?.repeat
19+
20+
const nLeadingSpaces = node.range[0] - ctx.comment.range[1] - 1
21+
const text = ctx.context.sourceCode.getText(node, nLeadingSpaces)
22+
const lines = text.split('\n')
23+
const symbolIndices: number[] = []
24+
25+
const nSymbols = alignmentSymbols.length
26+
if (nSymbols === 0)
27+
return ctx.reportError('No alignment symbols provided')
28+
29+
const n = repeat ? Number.MAX_SAFE_INTEGER : nSymbols
30+
let lastPos = 0
31+
for (let i = 0; i < n && i < 20; i++) {
32+
const symbol = alignmentSymbols[i % nSymbols]
33+
const maxIndex = lines.reduce((maxIndex, line) =>
34+
Math.max(line.indexOf(symbol, lastPos), maxIndex), -1)
35+
symbolIndices.push(maxIndex)
36+
37+
if (maxIndex < 0) {
38+
if (!repeat)
39+
return ctx.reportError(`Alignment symbol "${symbol}" not found`)
40+
else
41+
break
42+
}
43+
44+
for (let j = 0; j < lines.length; j++) {
45+
const line = lines[j]
46+
const index = line.indexOf(symbol, lastPos)
47+
if (index < 0)
48+
continue
49+
if (index !== maxIndex) {
50+
const padding = maxIndex - index
51+
lines[j] = line.slice(0, index) + ' '.repeat(padding) + line.slice(index)
52+
}
53+
}
54+
lastPos = maxIndex + symbol.length
55+
}
56+
57+
const modifiedText = lines.join('\n')
58+
if (text === modifiedText)
59+
return
60+
61+
ctx.report({
62+
node,
63+
message: 'Keep aligned',
64+
removeComment: false,
65+
fix: fixer => fixer.replaceText(node, modifiedText.slice(nLeadingSpaces)),
66+
})
67+
},
68+
}

0 commit comments

Comments
 (0)