-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add logic for reflowing lines and preserving data when resizing #609
Conversation
Very nice approach, love it. I found some issues though:
|
Thank you so much for this, @LucianBuzzo! |
This is possible because the pty normally just inserts '\r' at EOL. The actual newline step is inserted by the emulator itself with the autowrap setting (without '\n' char). By ignoring the '\r' at EOL the lines still belong together. From there you can flow lines in both direction - shrinking and enlarging. |
@mofux on your first point, one of the more tricky issues that arises when implementing this is that when lines wrap and reflow and are no longer fixed, the number of rows expand and contract and the viewport/scrollbar needs to be made aware of it. Scrolling the mouse wheel one notch should jump a consistent number of actual lines as are currently wrapped, not the amount they originated with. Consider the extreme case which is quite reasonable in a terminal environment of a huge line that consumes the entire viewport, if the terminal is then resized to 50% of the width and a reflow occurs (so the content consumes 2x viewports), you must be able to scroll a consistent number of rows. Based on your first comment it would seem that only the first 1000 rows are shown and the scroll bar isn't touched at all. We should probably just trim rows that go beyond the scrollback when the terminal width is reduced. On your second issue I would guess it's related to the pty as @jerch indicates, if that can be worked around that would be cool 😄 |
@LucianBuzzo You can distinct the EOLs in the write method - any real EOL will be triggered by a '\n' char in the data, therefore any line jumped to by '\n' is a real new line. All other lines are autowrapped and can reflow to a single line by enlarging. Note - that will not work with programs that use the terminal as canvas with heavy cursor jump usage (like vim or any nurses stuff), but since most of those implement the WINCH signal, they will "autorepair" itself afterwards. If they dont, hmm the output will be broken anyways. It might be possible to circumvent reflow for "jumped" lines at all and go with the cropping thingy you mentioned above. |
I'm probably thinking to naive here, but given what @jerch is saying, if we were only creating a new line if we see '\n', and then let the browser do the wrapping for us, wouldn't that work? Like .xterm-rows > div {
word-break: break-all;
} would then automatically force the characters into a new virtual line if they don't fit into one. Not sure what this does to the xterm viewport though. |
@mofux This would work, but introduce tons of problems with the [col, row] grid idea of a terminal and make the cursor jump escape sequences very hard to calculate. Imho an easier approach would be to mark all terminal lines as "real EOL" on a line level (pseudocode):
and set this to
During resizing the reflow logic would just have to search for a line with The tricky part is the handling of curses like apps without WINCH. A heuristic approach would be to set eol to |
@mofux doing it that way would be ideal in many respects such getting links spanning multiple rows trivially. However, I imagine there's a bunch of problems with doing it as @jerch mentions so it's probably better to make the links more clever instead. Another potential problem area for this is when I get "virtual selection" working #207, if things are even a pixel off it would look wrong. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work @LucianBuzzo, let me know if you have any questions.
src/Renderer.ts
Outdated
|
||
// If there are overflow lines use the last one | ||
if (overflowBuffer.length) { | ||
line = overflowBuffer.pop(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this would suffer from the issue I mentioned in #609 (comment) where is a really long line in the terminal that is reflowed within a smaller width gets larger than the viewport, you won't be able to see the rows. And resulting from this scrolling will feel awkward because it will jump the amount of rows that the line is now wrapped, instead of the correct 1.
@@ -159,11 +185,15 @@ export class Renderer { | |||
for (; i < width; i++) { | |||
if (!line[i]) { | |||
// Continue if the character is not available, this means a resize is currently in progress | |||
continue; | |||
data = this._terminal.defAttr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
data
, ch
and ch_width
can be shared among each character, should we expose a Terminal.defaultCharacter
variable or something (perhaps function if defAttr changes). Terminal.blankLine
reuses these empty characters for a single line.
src/utils/LineWrap.ts
Outdated
* | ||
* @return {array} - The trimmed terminal line | ||
*/ | ||
export const trimBlank = (line, min, blank) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not necessarily in this PR, but it might make sense to pull all this into a new class BufferModel
which extends CircularList
, I've felt a class like that has been needed to encapsulate operations on the buffer for a while. This way we can potentially solve the scrolling problem I've mentioned by making the buffer itself aware of the reflows.
I've updated this PR to include a more complete line reflow solution that should fix the issues raised in comments here. |
src/Viewport.ts
Outdated
@@ -66,9 +67,10 @@ export class Viewport { | |||
* Updates dimensions and synchronizes the scroll area if necessary. | |||
*/ | |||
public syncScrollArea(): void { | |||
if (this.lastRecordedBufferLength !== this.terminal.lines.length) { | |||
let totalLines = this.terminal.lines.totalLinesAtWidth(this.terminal.cols); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
syncScrollArea
is too performance critical, we cannot call totalLinesAtWidth
here. I think Buffer needs some reflow
function to be called when cols
change, this will do all the heavy lifting of mapping the underlying CircularList
lines with the reflowed lines only when a resize occurs.
@LucianBuzzo is this ready for a full review? |
@Tyriar It's very close! I made some changes that dramatically improved performance and it seems to be working very well. |
I found a bug with Mac's default |
@mofux The latest commit should fix the issue you're seeing. |
@LucianBuzzo |
@mofux When resizing the terminal with vim running, vim appears to actually send the last character twice. Here is the log output straight from the socket |
Hmm so this problem doesn't happen with neovim. |
You are right, I can confirm this is only happening in |
@mofux I think there's a typo in your "big question" |
I am not sure if it is related, but when I change the pty name in demo/app.js from I actually wanted to test if a different term environment yields the same "duplicate character on line break" error that we are seeing in vim. |
@mofux, this issue should now be fixed for |
Another issue (edit: same as #609 (comment)):
Notice the line is wrapped incorrectly and the scroll bar is active: Master behavior: |
@Tyriar vim seems to be sending duplicate characters. Any ideas how to handle this or why it is happening? |
@LucianBuzzo I think this is related to DECAWM, vim seems to write over EOL. No clue why it does... Edit: vim does this also for |
I've been trying to find out what's causing this vim issue. I'm not sure that it's related to DECAWM and I've been unable to find out any info on the duplicate character behaviour. |
Here's the relevant code that causes this https://github.com/vim/vim/blob/master/src/screen.c#L5707 Update: Some digging with this new information turned up this illuminating SO thread. I should be able to start making some progress again now!
|
Hmmm.... my theory is that other terminals will also reflow the additional character into the next line, but it then gets overwritten because the cursor is manually positioned to where the extra character would be (see the tty raw data, there is a control sequence that manually sets the cursor to the start of this new line, in your example above |
@mofux, yep this is my conclusion as well. Currently, my implementation moves the cursor to the source row, rather than the rendered row, which is why it isn't overwriting the duped character. |
This thread has been inactive for about 10 days, so I just want to remind @LucianBuzzo that if you need any input at any point here, just ask 😄 ! I think that this is a great enhancement to the xterm.js library, so I will put priority on this, in order to help out. EDIT: There is no pressure here though. |
@parisk I pushed some commits a few days ago fixing the vim issue and the scroll to bottom issue reported by @Tyriar |
* see https://jsperf.com/math-ceil-vs-bitwise | ||
* | ||
* @param {number} n - The value to round up. | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style nit: be more concise here:
@param ...
@return ..
@example
...
let f = (n << 0); | ||
return f === n ? f : f + 1; | ||
} | ||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style nit: Empty line here
// TODO: needs a global min terminal width of 2 | ||
if (this._terminal.x + ch_width - 1 >= this._terminal.cols) { | ||
// autowrap - DECAWM | ||
if (this._terminal.wraparoundMode) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we still want to respect this
@@ -60,7 +61,7 @@ export interface ILinkifier { | |||
deregisterLinkMatcher(matcherId: number): boolean; | |||
} | |||
|
|||
interface ICircularList<T> { | |||
interface IBuffer<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keeping the IBuffer and ICircularList interfaces separate would be cleaner, you should be able to do this:
interface IBuffer extends ICircularList<string> {
...
}
Also I don't think IBuffer should be generic.
@@ -23,7 +23,8 @@ export interface ITerminal { | |||
textarea: HTMLTextAreaElement; | |||
ybase: number; | |||
ydisp: number; | |||
lines: ICircularList<string>; | |||
scrollBase: number; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It used to be an ICircularList<string>
but I'm not sure this is true, I think lines is an ICircularList<any[]>
? (Until we move xterm.js fully to TS)
@@ -1230,7 +1241,7 @@ Terminal.prototype.scrollToTop = function() { | |||
* Scrolls the display of the terminal to the bottom. | |||
*/ | |||
Terminal.prototype.scrollToBottom = function() { | |||
this.scrollDisp(this.ybase - this.ydisp); | |||
this.scrollDisp(this.scrollBase - this.ydisp); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the difference between ybase and scrollbase?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scrollBase
compensates for wrapped lines.
I'm closing this PR in favour of #644 |
Fixes #622