Skip to content

Commit 3675e39

Browse files
committed
Merge pull request #255 from parallaxinc/scroll-bottom
Terminal Receive pane scrolling improvements and buffer increase
2 parents 32d4c72 + 86f176f commit 3675e39

File tree

6 files changed

+128
-70
lines changed

6 files changed

+128
-70
lines changed

src/creators/receive.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ const {
44
RECEIVE
55
} = require('../constants/action-types');
66

7-
function receive(output){
7+
function receive(output, offset){
88
return {
99
type: RECEIVE,
1010
payload: {
11-
output
11+
output,
12+
offset
1213
}
1314
};
1415
}

src/lib/scroller.js

Lines changed: 105 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,80 @@
11
'use strict';
22

3-
var _ = require('lodash');
4-
5-
function generateContent(lines, start, end, minLength) {
6-
return _(lines)
7-
.slice(start, end)
8-
.thru(function(array){
9-
if(array.length < minLength){
10-
// pad whitespace at top of array
11-
return _(new Array(minLength - array.length))
12-
.fill('\u2009')
13-
.concat(array)
14-
.value();
15-
}else{
16-
return array;
17-
}
18-
})
19-
.map(function(line){
20-
if(line.length === 0){
21-
// insert a blank space to prevent pre omitting a trailing newline,
22-
// even though pre/pre-nowrap/pre-line are specified.
23-
return '\u2009';
24-
}
25-
return line;
26-
})
27-
.join('\n');
28-
}
3+
const _ = require('lodash');
294

305
function Scroller(consoleElement) {
316
this.lines = [];
32-
this.minVisible = 30;
7+
this.lineOffset = 0;
8+
this.visibleCount = 50;
339
this.startPosition = 0;
10+
this.endPosition = 0;
3411
this.animateRequest = null;
3512
this.sticky = true;
3613
this.jumpToBottom = true;
3714
this.dirty = false;
3815
this.console = consoleElement;
16+
this.expandDistance = 200;
3917

4018
//pre-bind functions and throttle expansion
4119
this.refresh = this._renderVisible.bind(this);
4220
this.scroll = this._onScroll.bind(this);
43-
this.expand = _.throttle(this._expand.bind(this), 150, {
21+
this.expandTop = _.throttle(this._expandTop.bind(this), 150, {
22+
leading: true,
23+
trailing: true
24+
});
25+
this.expandBottom = _.throttle(this._expandBottom.bind(this), 150, {
4426
leading: true,
4527
trailing: true
4628
});
4729
}
4830

49-
Scroller.prototype.setLines = function(newLines) {
50-
var len = newLines.length;
31+
Scroller.prototype._generateContent = function(){
32+
return _(this.lines)
33+
.slice(this.startPosition - this.lineOffset, this.endPosition - this.lineOffset)
34+
.map(function(line){
35+
if(line.length === 0){
36+
// insert a blank space to prevent pre omitting a trailing newline,
37+
// even though pre/pre-nowrap/pre-line are specified.
38+
return '\u2009';
39+
}
40+
return line;
41+
})
42+
.join('\n');
43+
};
44+
45+
Scroller.prototype.setLines = function(newLines, offset) {
5146
this.lines = newLines;
47+
this.lineOffset = offset;
5248
if(this.sticky){
53-
this.startPosition = Math.max(0, len - this.minVisible);
54-
}else if(len === 1 && newLines[0].length === 0){
49+
this.endPosition = this.lineCount();
50+
this.startPosition = Math.max(this.lineOffset, this.endPosition - this.visibleCount);
51+
if(this.endPosition <= this.visibleCount){
52+
// follow text during initial 50 lines
53+
this.jumpToBottom = true;
54+
}
55+
}else if(newLines.length === 1 && newLines[0].length === 0){
5556
// ^^ `lines` is reset to an array with one empty line. ugh.
5657

5758
// handle the reset case when lines is replaced with an empty array
5859
// we don't have a direct event that can call this
5960
this.reset();
60-
}else if(len < this.startPosition){
61-
// handle buffer rollover, where number of lines will go from 2048 to ~1900
62-
this.startPosition = Math.max(0, len - this.minVisible);
61+
}else if(this.lineOffset > this.startPosition){
62+
// when buffer trims and we are now below the trimmed area, move up by difference
63+
const lineDiff = this.lineOffset - this.startPosition;
64+
this.startPosition += lineDiff;
65+
this.endPosition += lineDiff;
6366
}
6467
this.dirty = true;
6568
};
6669

70+
Scroller.prototype.lineCount = function(){
71+
return this.lines.length + this.lineOffset;
72+
};
73+
6774
Scroller.prototype.reset = function(){
68-
this.startPosition = Math.max(0, this.lines.length - this.minVisible);
75+
this.endPosition = Math.max(0, this.lineCount());
76+
this.startPosition = Math.max(0, this.endPosition - this.visibleCount);
77+
this.lineOffset = 0;
6978
this.jumpToBottom = true;
7079
this.sticky = true;
7180
this.dirty = true;
@@ -80,34 +89,61 @@ Scroller.prototype.requestRefresh = function(){
8089
Scroller.prototype._renderVisible = function(){
8190
this.animateRequest = null;
8291
if(this.dirty && this.console){
83-
var top = this.console.scrollTop;
92+
const top = this.console.scrollTop;
8493
if(this.sticky){
85-
this.startPosition = Math.max(0, this.lines.length - this.minVisible);
94+
this.endPosition = this.lineCount();
95+
this.startPosition = Math.max(this.lineOffset, this.endPosition - this.visibleCount);
8696
}
87-
this.console.innerHTML = generateContent(this.lines, this.startPosition, this.lines.length, this.minVisible);
97+
this.console.innerHTML = this._generateContent();
8898
if(this.jumpToBottom){
89-
this.console.scrollTop = 2000;
99+
this.console.scrollTop = 4000;
90100
this.jumpToBottom = false;
91-
}else if(!this.sticky && this.startPosition > 0 && top === 0){
101+
}else if(!this.sticky && this.startPosition > this.lineOffset && top === this.lineOffset){
92102
//cover the situation where the window was fully scrolled faster than expand could keep up and locked to the top
93-
requestAnimationFrame(this.expand);
103+
requestAnimationFrame(this.expandTop);
94104
}
95105
this.dirty = false;
96106
}
97107
};
98108

99-
Scroller.prototype._expand = function(){
100-
this.startPosition = Math.max(0, this.startPosition - this.minVisible);
101-
this.sticky = false;
109+
Scroller.prototype._expandTop = function(){
110+
this.startPosition = Math.max(this.lineOffset, this.startPosition - this.visibleCount);
102111
if(this.console){
103-
var scrollHeight = this.console.scrollHeight;
104-
var scrollTop = this.console.scrollTop;
112+
this.sticky = false;
113+
const scrollHeight = this.console.scrollHeight;
114+
const scrollTop = this.console.scrollTop;
105115

106116
// do an inline scroll to avoid potential scroll interleaving
107-
this.console.innerHTML = generateContent(this.lines, this.startPosition, this.lines.length, this.minVisible);
108-
var newScrollHeight = this.console.scrollHeight;
117+
this.console.innerHTML = this._generateContent();
118+
const newScrollHeight = this.console.scrollHeight;
109119
this.console.scrollTop = scrollTop + newScrollHeight - scrollHeight;
110120

121+
const oldEndPos = this.endPosition;
122+
this.endPosition = Math.min(this.endPosition, this.startPosition + (this.visibleCount * 2));
123+
124+
this.dirty = oldEndPos !== this.endPosition;
125+
if(this.dirty && !this.animateRequest){
126+
this.animateRequest = requestAnimationFrame(this.refresh);
127+
}
128+
}
129+
};
130+
131+
Scroller.prototype._expandBottom = function(){
132+
this.endPosition = Math.min(this.lineCount(), this.endPosition + this.visibleCount);
133+
if(this.console){
134+
// add the new content to the bottom, then get scroll position to remove content
135+
this.console.innerHTML = this._generateContent();
136+
const scrollHeight = this.console.scrollHeight;
137+
const scrollTop = this.console.scrollTop;
138+
139+
// update start position and render
140+
this.startPosition = Math.max(this.lineOffset, Math.min(this.lineCount() - (this.visibleCount * 2), this.endPosition - (this.visibleCount * 2)));
141+
this.console.innerHTML = this._generateContent();
142+
143+
// use difference to scroll offset
144+
const newScrollHeight = this.console.scrollHeight;
145+
this.console.scrollTop = scrollTop - (scrollHeight - newScrollHeight);
146+
111147
this.dirty = false;
112148
}
113149
};
@@ -117,23 +153,33 @@ Scroller.prototype._onScroll = function(){
117153
// do nothing, prepare to jump
118154
return;
119155
}
120-
var height = this.console.offsetHeight;
121-
var scrollHeight = this.console.scrollHeight;
122-
var scrollTop = this.console.scrollTop;
156+
const height = this.console.offsetHeight;
157+
const scrollHeight = this.console.scrollHeight;
158+
const scrollTop = this.console.scrollTop;
159+
const nearTop = scrollTop < this.expandDistance;
160+
const nearBottom = scrollTop + height > scrollHeight - this.expandDistance;
161+
const nearSticky = scrollTop + height > scrollHeight - 10;
162+
123163
if(this.sticky){
124-
if(scrollTop + height < scrollHeight - 30){
164+
if(!nearSticky){
125165
this.sticky = false;
126166
}
127167
}else{
128-
if(scrollTop < 15 && this.startPosition > 0){
129-
this.expand();
130-
}else if(scrollTop + height > scrollHeight - 30){
131-
this.jumpToBottom = true;
132-
this.sticky = true;
133-
this.dirty = true;
168+
if(nearTop && this.startPosition > this.lineOffset){
169+
this.expandTop();
170+
}else if(nearBottom){
171+
if(this.endPosition < this.lineCount() - 2){
172+
this.expandBottom();
173+
}else if(nearSticky){
174+
this.jumpToBottom = true;
175+
this.sticky = true;
176+
this.dirty = true;
177+
}
134178
}
135179
}
136180

181+
182+
137183
if(this.dirty && !this.animateRequest){
138184
this.animateRequest = requestAnimationFrame(this.refresh);
139185
}

src/lib/terminal.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ class Terminal {
1010
lastRefresh: 0,
1111
lines: [''],
1212
lineWrap: 256,
13-
maxLines: 2048,
13+
lineOffset: 0,
14+
maxLines: 10000,
1415
pointerLine: 0,
1516
pointerColumn: 0,
1617
refreshDelayMillis: 64,
@@ -31,6 +32,7 @@ class Terminal {
3132
const { refreshQueued } = this.state;
3233

3334
this.state.lines = [''];
35+
this.state.lineOffset = 0;
3436
this.state.pointerLine = 0;
3537
this.state.pointerColumn = 0;
3638

@@ -68,7 +70,7 @@ class Terminal {
6870

6971
setCursorPosition(line, col){
7072
const { lines } = this.state;
71-
for(var ix = lines.length; ix <= line; ix++){
73+
for(let ix = lines.length; ix <= line; ix++){
7274
lines[ix] = '';
7375
}
7476
this.state.pointerLine = Math.max(0, line);
@@ -195,6 +197,7 @@ class Terminal {
195197
if(lines.length > maxLines){
196198
const newLines = lines.slice(trimCount);
197199
this.state.lines = newLines;
200+
this.state.lineOffset = this.state.lineOffset + trimCount;
198201
this.state.pointerLine = Math.max(0, pointerLine - trimCount);
199202
this.state.pointerColumn = pointerColumn;
200203
} else {
@@ -207,6 +210,10 @@ class Terminal {
207210
return this.state.lines;
208211
}
209212

213+
getOffset(){
214+
return this.state.lineOffset;
215+
}
216+
210217
}
211218

212219
module.exports = Terminal;

src/plugins/handlers.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,8 @@ function handlers(app, opts, done){
403403
function updateTerminal(msg){
404404
terminal.refreshBuffer(msg, function(){
405405
const output = terminal.getLines();
406-
store.dispatch(creators.receive(output));
406+
const offset = terminal.getOffset();
407+
store.dispatch(creators.receive(output, offset));
407408
});
408409
}
409410

src/reducers/transmission.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@ const {
1212
const initial = {
1313
input: '',
1414
// output is an array of lines
15-
output: []
15+
output: [],
16+
// offset is number of lines cleared from console buffer
17+
offset: 0
1618
};
1719

1820
function transmission(state = initial, { type, payload }){
1921
switch(type){
2022
case CONNECT:
2123
return _.assign({}, state, { input: '' });
2224
case RECEIVE:
23-
return _.assign({}, state, { output: payload.output });
25+
return _.assign({}, state, { output: payload.output, offset: payload.offset });
2426
case TRANSMIT:
2527
return _.assign({}, state, { input: payload.input });
2628
case CLEAR_TRANSMISSION:

src/views/terminal.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ const styles = {
2222
class TermnialView extends React.Component {
2323

2424
componentWillReceiveProps(nextProps){
25-
const { output } = nextProps;
26-
this.scroller.setLines(output);
25+
const { output, offset } = nextProps;
26+
this.scroller.setLines(output, offset);
2727
this.scroller.requestRefresh();
2828
}
2929

@@ -72,13 +72,14 @@ module.exports = createContainer(TermnialView, {
7272

7373
getPropsFromStores({ store }){
7474
const { transmission, device } = store.getState();
75-
const { input, output } = transmission;
75+
const { input, output, offset } = transmission;
7676
const { connected } = device;
7777

7878
return {
7979
connected,
8080
input,
81-
output
81+
output,
82+
offset
8283
};
8384
}
8485
});

0 commit comments

Comments
 (0)