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

Commit 04ab48a

Browse files
committed
Basic understanding of tab characters
- We can now correctly indent with tabs instead of spaces if the editor settings desire it. - We now understand how tab characters that appear in the middle of a paragraph affect the paragraph's visual flow. Still to come: dealing with a mix of tab and space indentation.
1 parent 2cb1171 commit 04ab48a

File tree

2 files changed

+148
-58
lines changed

2 files changed

+148
-58
lines changed

lib/magic-reflow.js

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,15 @@ function last_in_range(array, start, end, cb) {
6060
return start - 1;
6161
}
6262

63-
function vlen(text, start_col, tab_len) {
63+
function vlen_of_tab_at(start_col, tab_vlen) {
64+
// Returns the visual length of a tab character, if that tab character
65+
// appears at the specified /start_col/. The /tab_vlen/ parameter indicates
66+
// the visual length of tab characters generally (e.g. 1 tab = 8 spaces).
67+
68+
return tab_vlen - (start_col % tab_vlen);
69+
}
70+
71+
function vlen(text, start_col, tab_vlen) {
6472
// Compute the visual length of /text/, assuming (for the purposes of tab
6573
// alignment) it starts at column /start_col/. The /text/ should not
6674
// contain any newlines.
@@ -69,7 +77,7 @@ function vlen(text, start_col, tab_len) {
6977
for (let t of text) {
7078
switch (t) {
7179
case '\t':
72-
vl = (((vl / tab_len)|0) + 1) * tab_len;
80+
vl += vlen_of_tab_at(vl, tab_vlen);
7381
break;
7482
case '\n':
7583
case '\r':
@@ -82,6 +90,24 @@ function vlen(text, start_col, tab_len) {
8290
return vl - start_col;
8391
}
8492

93+
function indent_with_tabs(vlen, start_col, tab_vlen) {
94+
// 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.
97+
98+
let i = '';
99+
let tv = vlen_of_tab_at(start_col, tab_vlen);
100+
if (tv <= vlen) {
101+
i = '\t';
102+
vlen -= tv;
103+
}
104+
if (vlen >= tab_vlen) {
105+
tv = (vlen/tab_vlen)|0;
106+
i = i.concat('\t'.repeat(tv));
107+
vlen -= tv * tab_vlen;
108+
}
109+
return i.concat(' '.repeat(vlen));
110+
}
85111

86112

87113
// Match a line with LINE_PATT and build an object describing what's in it.
@@ -198,6 +224,20 @@ export default {
198224
// How long should a line be, overall?
199225
line_vlen: atom.config.get('editor.preferredLineLength',
200226
{scope: editor.getRootScopeDescriptor()}),
227+
228+
// What is the starting (visual) column? We will assume that there
229+
// is a prefix that is this many columns wide, so we'll wrap the
230+
// text to fit in (line_vlen - start_col).
231+
//
232+
// We need both start_col and line_vlen so we can tell how much
233+
// visual space a leading tab takes up. If we simply subtracted
234+
// from line_vlen as we process indented text, we wouldn't be able
235+
// to compute the visual length of tab characters correctly.
236+
start_col: 0,
237+
238+
// When adding leading whitespace, should we use spaces or tabs?
239+
soft_tab: atom.config.get('editor.softTabs',
240+
{scope: editor.getRootScopeDescriptor()}),
201241
};
202242

203243
let text = this.reflow(editor.getTextInRange(range), state);
@@ -212,6 +252,8 @@ export default {
212252

213253
if (! state.line_vlen) state.line_vlen = 80;
214254
if (! state.tab_vlen) state.tab_vlen = 8;
255+
if (! state.start_col) state.start_col = 0;
256+
if (state.soft_tab === undefined) state.soft_tab = true;
215257

216258
let [head, body, tail] = this.head_body_tail(text);
217259
let useTabs = body.indexOf('\t') != -1;
@@ -261,7 +303,10 @@ export default {
261303
if (debug) debug(`retrying without leading: ${leading}`);
262304

263305
let reflowed = this.remove_and_reflow(leading, lines, state);
264-
let indent = ' '.repeat(leading.length);
306+
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);
265310

266311
let first = true;
267312
for (let line of reflowed) {
@@ -274,8 +319,11 @@ export default {
274319
remove_and_reflow(prefix, lines, state) {
275320
return this.reflow_lines(Array.from(
276321
this.remove_prefix_and_ws(prefix, lines)), {
277-
line_vlen: state.line_vlen - prefix.length,
322+
line_vlen: state.line_vlen,
278323
tab_vlen: state.tab_vlen,
324+
start_col: state.start_col
325+
+ vlen(prefix, state.start_col, state.tab_vlen),
326+
soft_tab: state.soft_tab,
279327
});
280328
},
281329

@@ -490,7 +538,7 @@ export default {
490538
}
491539

492540
// This is a garden-variety paragraph. Wrap it accordingly.
493-
return [end, this.wrap_para(para.join('\n'), state.line_vlen)];
541+
return [end, this.wrap_para(para.join('\n'), state)];
494542
},
495543

496544
consume_single_line(lines, start, startm, state) {
@@ -506,17 +554,17 @@ export default {
506554

507555
} else {
508556
// Garden-variety paragraph, no whitespace; reflow it.
509-
gen = this.wrap_para(lines[start], state.line_vlen);
557+
gen = this.wrap_para(lines[start], state);
510558
}
511559

512560
return [start + 1, gen];
513561
},
514562

515-
wrap_para: function*(text, fill, firstFill) {
563+
wrap_para: function*(text, state) {
516564
// Wrap /text/ as a single paragraph, yielding lines of wrapped text
517565
// that are no longer than /fill/.
518566

519-
if (debug) debug('wrapping paragraph:', text);
567+
if (debug) debug('wrapping paragraph:', text, state);
520568

521569
// XXX firstFill isn't used anymore, with the support for leading
522570
// indents gone.
@@ -529,34 +577,35 @@ export default {
529577
let res = [];
530578
let start = 0;
531579
let end = start;
532-
let curFill = firstFill ? firstFill : fill;
580+
let vl = state.start_col;
533581

534-
for (let [ws, word] of this.segments_of(text)) {
582+
for (let [run, ws, word] of this.segments_of(text)) {
535583
let newend = end + ws.length + word.length;
584+
let newvl = vl + vlen(run, vl, state.tab_vlen);
536585

537586
if (end == start) {
538587
// Current line is empty. Skip over leading whitespace that
539588
// would otherwise get put at the beginning of the line.
540589
start += ws.length;
590+
newvl = vl + vlen(word, vl, state.tab_vlen);
541591

542-
} else if (newend > start + curFill) {
592+
} else if (newvl > state.line_vlen) {
543593
// Current line is full. Emit what we have and start the next
544594
// line.
545-
let res = text.substr(start, end - start);
546-
yield res;
547-
curFill = fill; // no longer on the first line
595+
yield text.substr(start, end - start);
548596

549597
// Skip leading whitespace on the next line, and start the line
550598
// with the current word.
551599
start = end + ws.length;
600+
newvl = state.start_col + vlen(word, vl, state.tab_vlen);
552601
}
553602

554603
end = newend;
604+
vl = newvl;
555605
}
556606

557607
if (start != end) {
558-
let res = text.substr(start, end - start);
559-
yield res;
608+
yield text.substr(start, end - start);
560609
}
561610
},
562611

@@ -599,15 +648,18 @@ export default {
599648
},
600649

601650
segments_of: function*(text) {
602-
// Split /text/ at word boundaries, yielding [ws, word] pairs where /ws/
603-
// is the whitespace that appears before /word/.
651+
// Split /text/ at word boundaries, yielding [run, ws, word] tuples
652+
// where /ws/ is the whitespace that appears before /word/, and /run/ is
653+
// ws.concat(word).
604654

605655
let span = /(\s*)(\S+)/gm;
606656
let m;
607657
while (m = span.exec(text)) {
608-
yield [m[1], m[2]];
658+
yield m;
609659
}
610660
},
611661

612-
vlen: vlen, // for testing purposes
662+
// Exports for testing purposes, used in the spec file
663+
vlen: vlen,
664+
indent_with_tabs: indent_with_tabs,
613665
};

spec/magic-reflow-spec.js

Lines changed: 76 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,68 +7,96 @@ import MagicReflow from '../lib/magic-reflow';
77
// To run a specific `it` or `describe` block add an `f` to the front (e.g.
88
// `fit` or `fdescribe`). Remove the `f` to unfocus the block.
99

10-
function test([input, line_vlen, tab_vlen], expected) {
10+
function test([input, line_vlen, tab_vlen, soft_tab], expected) {
1111
console.log(`BEGIN TEST:\n${input}`);
1212
let actual = MagicReflow.reflow(
13-
input, {line_vlen: line_vlen, tab_vlen: tab_vlen});
13+
input, {line_vlen: line_vlen, tab_vlen: tab_vlen, soft_tab: soft_tab});
1414
console.log(`TEST EXPECTED:\n${expected}`);
1515
console.log(`TEST ACTUAL:\n${[actual]}`);
1616
expect(actual).toBe(expected);
1717
};
1818

19-
function test_vlen(args, expected) {
19+
function qcheck(what, args, expected) {
2020
it(`returns ${expected} for ${args}`, () => {
21-
let actual = MagicReflow.vlen(...args);
21+
let actual = what(...args);
2222
expect(actual).toBe(expected);
2323
});
2424
}
2525

2626
describe('MagicReflow', () => {
2727
describe('vlen', () => {
28+
let vlen = MagicReflow.vlen;
2829
describe('empty strings', () => {
29-
test_vlen(['', 0, 8], 0);
30-
test_vlen(['', 4, 8], 0);
30+
qcheck(vlen, ['', 0, 8], 0);
31+
qcheck(vlen, ['', 4, 8], 0);
3132
});
3233

3334
describe('non-tab chars', () => {
34-
test_vlen([' ', 0, 4], 2);
35-
test_vlen(['foo', 0, 4], 3);
35+
qcheck(vlen, [' ', 0, 4], 2);
36+
qcheck(vlen, ['foo', 0, 4], 3);
3637
});
3738

3839
describe('simple tabs', () => {
39-
test_vlen(['\t', 0, 8], 8);
40-
test_vlen(['\t', 4, 8], 4);
41-
test_vlen(['\t', 8, 8], 8);
40+
qcheck(vlen, ['\t', 0, 8], 8);
41+
qcheck(vlen, ['\t', 4, 8], 4);
42+
qcheck(vlen, ['\t', 8, 8], 8);
4243

43-
test_vlen(['\t', 0, 4], 4);
44-
test_vlen(['\t', 4, 4], 4);
44+
qcheck(vlen, ['\t', 0, 4], 4);
45+
qcheck(vlen, ['\t', 4, 4], 4);
4546
});
4647

4748
describe('short text followed by tab', () => {
48-
test_vlen(['foo\t', 0, 8], 8);
49-
test_vlen(['foo\t', 4, 8], 4);
50-
test_vlen(['foo\t', 8, 8], 8);
49+
qcheck(vlen, ['foo\t', 0, 8], 8);
50+
qcheck(vlen, ['foo\t', 4, 8], 4);
51+
qcheck(vlen, ['foo\t', 8, 8], 8);
5152

52-
test_vlen(['foo\t', 0, 4], 4);
53-
test_vlen(['foo\t', 4, 4], 4);
53+
qcheck(vlen, ['foo\t', 0, 4], 4);
54+
qcheck(vlen, ['foo\t', 4, 4], 4);
5455
});
5556

5657
describe('tab in middle', () => {
57-
test_vlen(['foo\tbar', 0, 8], 8+3);
58-
test_vlen(['foo\tbar', 4, 8], 4+3);
59-
test_vlen(['foo\tbar', 8, 8], 8+3);
58+
qcheck(vlen, ['foo\tbar', 0, 8], 8+3);
59+
qcheck(vlen, ['foo\tbar', 4, 8], 4+3);
60+
qcheck(vlen, ['foo\tbar', 8, 8], 8+3);
6061

61-
test_vlen(['foo\tbar', 0, 4], 4+3);
62-
test_vlen(['foo\tbar', 4, 4], 4+3);
62+
qcheck(vlen, ['foo\tbar', 0, 4], 4+3);
63+
qcheck(vlen, ['foo\tbar', 4, 4], 4+3);
6364
});
6465

6566
describe('multiple tabs', () => {
66-
test_vlen(['foo\t\t', 0, 8], 8+8);
67-
test_vlen(['foo\t\t', 4, 8], 4+8);
68-
test_vlen(['foo\t\t', 8, 8], 8+8);
67+
qcheck(vlen, ['foo\t\t', 0, 8], 8+8);
68+
qcheck(vlen, ['foo\t\t', 4, 8], 4+8);
69+
qcheck(vlen, ['foo\t\t', 8, 8], 8+8);
6970

70-
test_vlen(['foo\t\t', 0, 4], 4+4);
71-
test_vlen(['foo\t\t', 4, 4], 4+4);
71+
qcheck(vlen, ['foo\t\t', 0, 4], 4+4);
72+
qcheck(vlen, ['foo\t\t', 4, 4], 4+4);
73+
});
74+
});
75+
76+
describe('indent_with_tabs', () => {
77+
let iwt = MagicReflow.indent_with_tabs;
78+
79+
describe('empty strings', () => {
80+
qcheck(iwt, [0, 0, 4], '');
81+
qcheck(iwt, [0, 1, 4], '');
82+
});
83+
84+
describe('first tab', () => {
85+
qcheck(iwt, [4, 0, 4], '\t');
86+
qcheck(iwt, [3, 1, 4], '\t');
87+
});
88+
89+
describe('multiple tabs', () => {
90+
qcheck(iwt, [8, 0, 4], '\t\t');
91+
qcheck(iwt, [6, 2, 4], '\t\t');
92+
});
93+
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 ');
72100
});
73101
});
74102

@@ -184,24 +212,34 @@ describe('MagicReflow', () => {
184212
));
185213
});
186214

187-
// XXX Need to implement visual-width calculations to handle tabs... :/
188-
xdescribe('when dealing with leading tab characters', () => {
189-
it('preserves leading indents with tabs (one line)', () => test(
190-
['\tLeading tab.\nSecond line.', 40],
191-
'\tLeading tab. Second line.'
192-
));
215+
describe('when dealing with leading tab characters', () => {
193216
it('preserves block style with tabs (one line)', () => test(
194217
['\tLeading tab.\n\tSecond line.', 40],
195218
'\tLeading tab. Second line.'
196219
));
197-
it('preserves leading indents with tabs (multi-line)', () => test(
198-
['\tLeading tab.\nSecond line.', 40],
199-
'\tLeading tab. Second line.'
200-
));
201220
it('preserves block style with tabs (multi-line)', () => test(
202221
['\tLeading tab that is a long line.\n\tSecond line.', 24, 8],
203222
'\tLeading tab that\n\tis a long line.\n\tSecond line.'
204223
));
224+
it('uses the correct tab width for indentation', () => test(
225+
['\tLeading tab that is a long line. Should be 24 cols.', 24, 4],
226+
'\tLeading tab that is\n\ta long line. Should\n\tbe 24 cols.',
227+
));
228+
it('handles tabs after leading sigils', () => test(
229+
['1.\tThis is a numbered list item that needs wrapping.', 24, 4, false],
230+
'1.\tThis is a numbered\n\tlist item that needs\n\twrapping.'
231+
));
232+
233+
// BEGIN: Leading indents are not supported currently.
234+
xit('preserves leading indents with tabs (one line)', () => test(
235+
['\tLeading tab.\nSecond line.', 40],
236+
'\tLeading tab. Second line.'
237+
));
238+
xit('preserves leading indents with tabs (multi-line)', () => test(
239+
['\tLeading tab.\nSecond line.', 40],
240+
'\tLeading tab. Second line.'
241+
));
242+
// END: leading indents
205243
});
206244

207245
for (let sigil of ['#', '##', '//', '///', '--', ';', ';;', ';;;']) {

0 commit comments

Comments
 (0)