Skip to content

Commit c7571e2

Browse files
committed
readline: make tab size configurable
This adds the `tabSize` option to readline to allow different tab sizes.
1 parent efd5a6b commit c7571e2

File tree

3 files changed

+69
-4
lines changed

3 files changed

+69
-4
lines changed

doc/api/readline.md

+5
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,9 @@ the current position of the cursor down.
456456
<!-- YAML
457457
added: v0.1.98
458458
changes:
459+
- version: REPLACEME
460+
pr-url: https://github.com/nodejs/node/pull/31318
461+
description: The `tabSize` option is supported now.
459462
- version: v8.3.0, 6.11.4
460463
pr-url: https://github.com/nodejs/node/pull/13497
461464
description: Remove max limit of `crlfDelay` option.
@@ -499,6 +502,8 @@ changes:
499502
can both form a complete key sequence using the input read so far and can
500503
take additional input to complete a longer key sequence).
501504
**Default:** `500`.
505+
* `tabSize` {integer} The number of spaces a tab is equal to (minimum 1).
506+
**Default:** `8`.
502507

503508
The `readline.createInterface()` method creates a new `readline.Interface`
504509
instance.

lib/readline.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ const {
4545
ERR_INVALID_CURSOR_POS,
4646
ERR_INVALID_OPT_VALUE
4747
} = require('internal/errors').codes;
48-
const { validateString } = require('internal/validators');
48+
const {
49+
validateString,
50+
validateUint32,
51+
} = require('internal/validators');
4952
const {
5053
inspect,
5154
getStringWidth,
@@ -105,6 +108,7 @@ function Interface(input, output, completer, terminal) {
105108
this._sawKeyPress = false;
106109
this._previousKey = null;
107110
this.escapeCodeTimeout = ESCAPE_CODE_TIMEOUT;
111+
this.tabSize = 8;
108112

109113
EventEmitter.call(this);
110114
let historySize;
@@ -118,6 +122,11 @@ function Interface(input, output, completer, terminal) {
118122
completer = input.completer;
119123
terminal = input.terminal;
120124
historySize = input.historySize;
125+
if (input.tabSize !== undefined) {
126+
const positive = true;
127+
validateUint32(input.tabSize, 'tabSize', positive);
128+
this.tabSize = input.tabSize;
129+
}
121130
removeHistoryDuplicates = input.removeHistoryDuplicates;
122131
if (input.prompt !== undefined) {
123132
prompt = input.prompt;
@@ -718,10 +727,9 @@ Interface.prototype._getDisplayPos = function(str) {
718727
offset = 0;
719728
continue;
720729
}
721-
// Tabs must be aligned by an offset of 8.
722-
// TODO(BridgeAR): Make the tab size configurable.
730+
// Tabs must be aligned by an offset of the tab size.
723731
if (char === '\t') {
724-
offset += 8 - (offset % 8);
732+
offset += this.tabSize - (offset % this.tabSize);
725733
continue;
726734
}
727735
const width = getStringWidth(char);

test/parallel/test-readline-interface.js

+52
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,39 @@ function assertCursorRowsAndCols(rli, rows, cols) {
138138
name: 'RangeError',
139139
code: 'ERR_INVALID_OPT_VALUE'
140140
});
141+
142+
// Check for invalid tab sizes.
143+
assert.throws(
144+
() => new readline.Interface({
145+
input,
146+
tabSize: 0
147+
}),
148+
{
149+
message: 'The value of "tabSize" is out of range. ' +
150+
'It must be >= 1 && < 4294967296. Received 0',
151+
code: 'ERR_OUT_OF_RANGE'
152+
}
153+
);
154+
155+
assert.throws(
156+
() => new readline.Interface({
157+
input,
158+
tabSize: '4'
159+
}),
160+
{ code: 'ERR_INVALID_ARG_TYPE' }
161+
);
162+
163+
assert.throws(
164+
() => new readline.Interface({
165+
input,
166+
tabSize: 4.5
167+
}),
168+
{
169+
code: 'ERR_OUT_OF_RANGE',
170+
message: 'The value of "tabSize" is out of range. ' +
171+
'It must be an integer. Received 4.5'
172+
}
173+
);
141174
}
142175

143176
// Sending a single character with no newline
@@ -632,6 +665,25 @@ function assertCursorRowsAndCols(rli, rows, cols) {
632665
rli.close();
633666
}
634667

668+
// Multi-line input cursor position and long tabs
669+
{
670+
const [rli, fi] = getInterface({ tabSize: 16, terminal: true, prompt: '' });
671+
fi.columns = 10;
672+
fi.emit('data', 'multi-line\ttext \t');
673+
assert.strictEqual(rli.cursor, 17);
674+
assertCursorRowsAndCols(rli, 3, 2);
675+
rli.close();
676+
}
677+
678+
// Check for the default tab size.
679+
{
680+
const [rli, fi] = getInterface({ terminal: true, prompt: '' });
681+
fi.emit('data', 'the quick\tbrown\tfox');
682+
assert.strictEqual(rli.cursor, 19);
683+
// The first tab is 7 spaces long, the second one 3 spaces.
684+
assertCursorRowsAndCols(rli, 0, 27);
685+
}
686+
635687
// Multi-line prompt cursor position
636688
{
637689
const [rli, fi] = getInterface({

0 commit comments

Comments
 (0)