Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.

Commit 4be8b76

Browse files
committed
Handle inconsistent leading tabs/spaces
Recognize and handle visually-equivalent mixes of tabs and spaces when used for leading indentation, and replace with the appropriate whitespace when reflowing (according to editor settings).
1 parent 04ab48a commit 4be8b76

File tree

2 files changed

+111
-40
lines changed

2 files changed

+111
-40
lines changed

lib/magic-reflow.js

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -74,41 +74,71 @@ function vlen(text, start_col, tab_vlen) {
7474
// contain any newlines.
7575

7676
let vl = start_col;
77-
for (let t of text) {
78-
switch (t) {
77+
78+
let re = /([^\t\r\n]+|[\t\r\n])/gm;
79+
let m;
80+
while (m = re.exec(text)) {
81+
switch (m[1]) {
7982
case '\t':
8083
vl += vlen_of_tab_at(vl, tab_vlen);
8184
break;
8285
case '\n':
8386
case '\r':
8487
throw "Text contains newlines";
8588
default:
86-
++vl;
89+
vl += m[1].length;
8790
break;
8891
}
8992
}
93+
9094
return vl - start_col;
9195
}
9296

93-
function indent_with_tabs(vlen, start_col, tab_vlen) {
97+
function indent_with_tabs(vlen, tab_vlen) {
9498
// Return a sequence of tab and space characters long enough to visually
95-
// indent from /start_col/ to /start_col + vlen/, assuming tab characters
96-
// are /tab_vlen/ columns wide.
99+
// indent from column 0 to /vlen/, assuming tab characters are /tab_vlen/
100+
// columns wide.
97101

98102
let i = '';
99-
let tv = vlen_of_tab_at(start_col, tab_vlen);
100-
if (tv <= vlen) {
101-
i = '\t';
102-
vlen -= tv;
103-
}
104103
if (vlen >= tab_vlen) {
105-
tv = (vlen/tab_vlen)|0;
106-
i = i.concat('\t'.repeat(tv));
107-
vlen -= tv * tab_vlen;
104+
let tabs = (vlen/tab_vlen)|0;
105+
i = i.concat('\t'.repeat(tabs));
106+
vlen -= tabs * tab_vlen;
108107
}
109108
return i.concat(' '.repeat(vlen));
110109
}
111110

111+
function* leading_tabs_to_spaces(lines, tab_vlen) {
112+
// Replaces leading tab characters in each line of /lines/ with a
113+
// visually-appropriate number of spaces. Assumes that tab characters are
114+
// typically /tab_vlen/ columns wide.
115+
116+
// This is O(n^2) becuase of the vlen() call, but that should be fine since
117+
// we're only operating on a single line.
118+
for (let line of lines) {
119+
// Fixup all leading whitespace, to catch weird corner cases like
120+
// " \t \tThe rest of the line"
121+
yield line.replace(/^\s+/, (ws) => {
122+
return ws.replace(/\t/g, (m, off, t) => {
123+
let vl = vlen(t.substr(0, off), 0, tab_vlen);
124+
return ' '.repeat(vlen_of_tab_at(vl, tab_vlen));
125+
});
126+
});
127+
}
128+
}
129+
130+
function* leading_spaces_to_tabs(lines, tab_vlen) {
131+
// Undoes what leading_tabs_to_spaces does -- replaces runs of space
132+
// characters with an appropriate number of tabs.
133+
134+
for (let line of lines) {
135+
yield line.replace(/^\s+/, (ws) => {
136+
return indent_with_tabs(vlen(ws, 0, tab_vlen), tab_vlen);
137+
});
138+
}
139+
}
140+
141+
112142

113143
// Match a line with LINE_PATT and build an object describing what's in it.
114144
function match_line(text) {
@@ -259,12 +289,20 @@ export default {
259289
let useTabs = body.indexOf('\t') != -1;
260290

261291
let lines = body.split(/\r\n|\r|\n/);
262-
let out = this.reflow_lines(lines, state);
292+
let out = this.reflow_lines(
293+
leading_tabs_to_spaces(lines, state.tab_vlen),
294+
state);
295+
296+
if (! state.soft_tab) {
297+
out = leading_spaces_to_tabs(out, state.tab_vlen);
298+
}
263299

264300
return head.concat(Array.from(out).join('\n'), tail);
265301
},
266302

267303
reflow_lines: function*(lines, state) {
304+
lines = Array.from(lines);
305+
268306
if (debug) debug('reflowing:', state, lines);
269307

270308
// Break the text into blocks, and reflow each block separately.
@@ -304,9 +342,7 @@ export default {
304342

305343
let reflowed = this.remove_and_reflow(leading, lines, state);
306344
let ilen = vlen(leading, state.start_col, state.tab_vlen);
307-
let indent = state.soft_tab
308-
? ' '.repeat(ilen)
309-
: indent_with_tabs(ilen, state.start_col, state.tab_vlen);
345+
let indent = ' '.repeat(ilen);
310346

311347
let first = true;
312348
for (let line of reflowed) {
@@ -662,4 +698,6 @@ export default {
662698
// Exports for testing purposes, used in the spec file
663699
vlen: vlen,
664700
indent_with_tabs: indent_with_tabs,
701+
leading_tabs_to_spaces: leading_tabs_to_spaces,
702+
leading_spaces_to_tabs: leading_spaces_to_tabs,
665703
};

spec/magic-reflow-spec.js

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ function qcheck(what, args, expected) {
2323
});
2424
}
2525

26+
function qcheckgen(gen, what, args, expected) {
27+
it(`${what}: returns ${expected} for ${args}`, () => {
28+
console.log(`BEGIN TEST ${what}:`, args);
29+
let actual = Array.from(gen(...args));
30+
console.log(`ACTUAL: `, actual);
31+
console.log(`EXPECTED:`, [expected]);
32+
expect(actual[0]).toBe(expected);
33+
});
34+
}
35+
2636
describe('MagicReflow', () => {
2737
describe('vlen', () => {
2838
let vlen = MagicReflow.vlen;
@@ -76,28 +86,30 @@ describe('MagicReflow', () => {
7686
describe('indent_with_tabs', () => {
7787
let iwt = MagicReflow.indent_with_tabs;
7888

79-
describe('empty strings', () => {
80-
qcheck(iwt, [0, 0, 4], '');
81-
qcheck(iwt, [0, 1, 4], '');
82-
});
89+
qcheck(iwt, [0, 4], '');
90+
qcheck(iwt, [1, 4], ' ');
91+
qcheck(iwt, [4, 4], '\t');
92+
qcheck(iwt, [5, 4], '\t ');
93+
qcheck(iwt, [8, 4], '\t\t');
94+
qcheck(iwt, [10, 4], '\t\t ');
95+
});
8396

84-
describe('first tab', () => {
85-
qcheck(iwt, [4, 0, 4], '\t');
86-
qcheck(iwt, [3, 1, 4], '\t');
87-
});
97+
describe('leading_tabs_to_spaces', () => {
98+
let gen = MagicReflow.leading_tabs_to_spaces;
8899

89-
describe('multiple tabs', () => {
90-
qcheck(iwt, [8, 0, 4], '\t\t');
91-
qcheck(iwt, [6, 2, 4], '\t\t');
92-
});
100+
qcheckgen(gen, 'tab', [['\tfoo'], 4], ' foo');
101+
qcheckgen(gen, 'tab+spc', [['\t foo'], 4], ' foo');
102+
qcheckgen(gen, 'spc+tab+spc', [[' \t foo'], 4], ' foo');
103+
qcheckgen(gen, 'spc+tab+spc+tab', [[' \t \tfoo'], 4], ' foo');
104+
});
93105

94-
describe('trailing spaces', () => {
95-
qcheck(iwt, [2, 0, 4], ' ');
96-
qcheck(iwt, [2, 2, 4], '\t');
97-
qcheck(iwt, [1, 2, 4], ' ');
98-
qcheck(iwt, [5, 2, 4], '\t ');
99-
qcheck(iwt, [5, 6, 4], '\t ');
100-
});
106+
describe('leading_spaces_to_tabs', () => {
107+
let gen = MagicReflow.leading_spaces_to_tabs;
108+
109+
qcheckgen(gen, 'tab', [[' foo'], 4], '\tfoo');
110+
qcheckgen(gen, 'tab+spc', [[' foo'], 4], '\t foo');
111+
qcheckgen(gen, 'spc+tab+spc', [[' foo'], 4], '\t foo');
112+
qcheckgen(gen, 'spc+tab+spc+tab', [[' foo'], 4], '\t\tfoo');
101113
});
102114

103115
describe('when reflowing a single paragraph', () => {
@@ -194,6 +206,17 @@ describe('MagicReflow', () => {
194206
' This is\n the first\n line. This\n is the\n second line.'
195207
));
196208

209+
it('converts spaces to tabs if appropriate', () => test(
210+
[' This is the first line.', 24, 4, false],
211+
'\tThis is the first line.'
212+
));
213+
214+
it('handles inconsistent use of tabs and spaces', () => test(
215+
['\tLeading tab.\n Second line.', 40, 8, true],
216+
' Leading tab. Second line.'
217+
));
218+
219+
197220
it('fixes inconsistent indentation', () => test(
198221
[`
199222
This is the first line.
@@ -214,22 +237,32 @@ describe('MagicReflow', () => {
214237

215238
describe('when dealing with leading tab characters', () => {
216239
it('preserves block style with tabs (one line)', () => test(
217-
['\tLeading tab.\n\tSecond line.', 40],
240+
['\tLeading tab.\n\tSecond line.', 40, 8, false],
218241
'\tLeading tab. Second line.'
219242
));
220243
it('preserves block style with tabs (multi-line)', () => test(
221-
['\tLeading tab that is a long line.\n\tSecond line.', 24, 8],
244+
['\tLeading tab that is a long line.\n\tSecond line.', 24, 8, false],
222245
'\tLeading tab that\n\tis a long line.\n\tSecond line.'
223246
));
224247
it('uses the correct tab width for indentation', () => test(
225-
['\tLeading tab that is a long line. Should be 24 cols.', 24, 4],
248+
['\tLeading tab that is a long line. Should be 24 cols.', 24, 4, false],
226249
'\tLeading tab that is\n\ta long line. Should\n\tbe 24 cols.',
227250
));
228251
it('handles tabs after leading sigils', () => test(
229252
['1.\tThis is a numbered list item that needs wrapping.', 24, 4, false],
230253
'1.\tThis is a numbered\n\tlist item that needs\n\twrapping.'
231254
));
232255

256+
it('converts tabs to spaces if appropriate', () => test(
257+
['\tLeading tab.\n\tSecond line.', 40, 8, true],
258+
' Leading tab. Second line.'
259+
));
260+
261+
it('handles inconsistent use of tabs and spaces', () => test(
262+
['\tLeading tab.\n Second line.', 40, 8, false],
263+
'\tLeading tab. Second line.'
264+
));
265+
233266
// BEGIN: Leading indents are not supported currently.
234267
xit('preserves leading indents with tabs (one line)', () => test(
235268
['\tLeading tab.\nSecond line.', 40],

0 commit comments

Comments
 (0)