Skip to content
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

Closed
wants to merge 11 commits into from
Closed

Add logic for reflowing lines and preserving data when resizing #609

wants to merge 11 commits into from

Conversation

LucianBuzzo
Copy link
Contributor

@LucianBuzzo LucianBuzzo commented Mar 16, 2017

Fixes #622

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.4%) to 67.535% when pulling e72d106 on LucianBuzzo:lb-line-wrap into 6d25706 on sourcelair:master.

@mofux
Copy link
Contributor

mofux commented Mar 16, 2017

Very nice approach, love it. I found some issues though:

  1. Create some output, then shrink the terminal by half of its previous cols, and scroll up. Even if the wrapped output is below 1000 lines of the scrollback buffer, I am unable so see the start of the scrollback.

  2. Run ls -alh with 80 cols. Shrink the terminal to 20 cols. run ls -alh again. Resize back to 80 cols. The second ls -alh output is still wrapped. This does not happen in Terminal.app, not sure how they do it, because technically the wrapped lines are arriving as-is from the pty process. I'm wondering if there's any control sequence that marks the end of the wrapped line?

wrap-unwrap

@blink1073
Copy link
Contributor

Thank you so much for this, @LucianBuzzo!

@LucianBuzzo
Copy link
Contributor Author

@mofux A simpler approach to this issue might be to just implement the same behaviour as hyperterm and ignore reflowing altogether.
TLDR hyperterm retains the line information but crops the line visually when resizing smaller.

@jerch
Copy link
Member

jerch commented Mar 16, 2017

Run ls -alh with 80 cols. Shrink the terminal to 20 cols. run ls -alh again. Resize back to 80 cols. The second ls -alh output is still wrapped. This does not happen in Terminal.app, not sure how they do it, because technically the wrapped lines are arriving as-is from the pty process. I'm wondering if there's any control sequence that marks the end of the wrapped line?

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.

@Tyriar
Copy link
Member

Tyriar commented Mar 16, 2017

@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
Copy link
Contributor Author

@Tyriar @mofux I think point 2 should be straightforward to fix, I'll take a look 👍

@jerch
Copy link
Member

jerch commented Mar 16, 2017

@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.

@parisk parisk mentioned this pull request Mar 17, 2017
@mofux
Copy link
Contributor

mofux commented Mar 17, 2017

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.

@jerch
Copy link
Member

jerch commented Mar 17, 2017

@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):

for line in terminal:
    line.eol = true;

and set this to false once a wrap happens in write (pseudocode):

write(s):
    ...
    if (wrapped):
        line.eol = false;
    ...

During resizing the reflow logic would just have to search for a line with eol == true and fit all following lines into the new width up to the next real EOL line or sumthing like that.

The tricky part is the handling of curses like apps without WINCH. A heuristic approach would be to set eol to true again as soon as a cursor jump happens into a line by an escape sequence. Another heuristic hint could be the ICANON flag of the pty, if it is available for the emulator.

@Tyriar
Copy link
Member

Tyriar commented Mar 17, 2017

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?

@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.

Copy link
Member

@Tyriar Tyriar left a 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();
Copy link
Member

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;
Copy link
Member

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.

*
* @return {array} - The trimmed terminal line
*/
export const trimBlank = (line, min, blank) => {
Copy link
Member

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.

@LucianBuzzo
Copy link
Contributor Author

I've updated this PR to include a more complete line reflow solution that should fix the issues raised in comments here.
I still need to fix sticky scrolling and add a caching/memoization layer in front of the buffer, but for the most part it seems to work quite well.

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);
Copy link
Member

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.

@Tyriar
Copy link
Member

Tyriar commented Mar 27, 2017

@LucianBuzzo is this ready for a full review?

@LucianBuzzo
Copy link
Contributor Author

@Tyriar It's very close! I made some changes that dramatically improved performance and it seems to be working very well.
Could you please give it a shakedown and see if there's anything obviously broken?

@mofux
Copy link
Contributor

mofux commented Mar 28, 2017

I found a bug with Mac's default top application, tested using the latest version of this pull request. Running top, then resizing the terminal to 40 cols starts to wrap the lines of tops output. In contrast, the default mac Terminal.app will truncate the lines. Anyway, when quitting top the terminal is dysfunctional, leaving you with characters all over the place, and I am unable to recover with clear.

top

@LucianBuzzo
Copy link
Contributor Author

@mofux The latest commit should fix the issue you're seeing.

@mofux
Copy link
Contributor

mofux commented Mar 29, 2017

@LucianBuzzo top works perfect now, thanks! Testing some more, I have found another issue with vim. If I write some multiline text into vim and then resize from 81 cols to 40 cols for example, the text is wrapped incorrectly, and strange things start to happen when typing some more...

vim

@LucianBuzzo
Copy link
Contributor Author

@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 �[m�[H�[2J�[1;30Hqwertt�[2;1Hy.
As you can see the t character in qwerty appears twice at the end of the line.
On the master branch the additional character is never rendered so it's not an issue.
I'm not sure why this happens and plan to do some more investigation.

@LucianBuzzo
Copy link
Contributor Author

Hmm so this problem doesn't happen with neovim.

@mofux
Copy link
Contributor

mofux commented Mar 30, 2017

You are right, I can confirm this is only happening in vim but not nvim. The big question is, why does the extra character that is sent not rendered?

@LucianBuzzo
Copy link
Contributor Author

@mofux I think there's a typo in your "big question"

@mofux
Copy link
Contributor

mofux commented Mar 30, 2017

I am not sure if it is related, but when I change the pty name in demo/app.js from xterm-color to vt100, I get an exception as soon as I start typing in vim:

buffer-error

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.

@LucianBuzzo
Copy link
Contributor Author

@mofux, this issue should now be fixed for vt100. I looked and we're getting the same vim output :(

@Tyriar
Copy link
Member

Tyriar commented Apr 1, 2017

Found an issue:

  1. Open demo
  2. Run ls
  3. Resize to 60 cols, the output should be wrapped such that there is no scroll bar. It should fill the space below the prompt when resized if there is "empty" space available

image

@Tyriar
Copy link
Member

Tyriar commented Apr 1, 2017

Another issue:

  1. Type in a lot of content into prompt, the output should wrap like normal and no wrap onto the same line (as in master)
  2. Backspace to remove the content

branch behavior

image

image

master behavior

image

image

@Tyriar
Copy link
Member

Tyriar commented Apr 1, 2017

Another issue (edit: same as #609 (comment)):

  1. Open vim
  2. Press i
  3. Paste in this: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse euismod urna quis nisi dapibus sodales tristique a ante. Nunc quis lectus dui.

Notice the line is wrapped incorrectly and the scroll bar is active:

image

Master behavior:

image

@LucianBuzzo
Copy link
Contributor Author

@Tyriar vim seems to be sending duplicate characters. Any ideas how to handle this or why it is happening?

@jerch
Copy link
Member

jerch commented Apr 2, 2017

@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 TERM=vt100 (other escape seq are quite different though)

@Tyriar Tyriar mentioned this pull request Apr 3, 2017
5 tasks
@LucianBuzzo
Copy link
Contributor Author

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.

@LucianBuzzo
Copy link
Contributor Author

LucianBuzzo commented Apr 11, 2017

Here's the relevant code that causes this https://github.com/vim/vim/blob/master/src/screen.c#L5707
I'm a bit puzzled as to how to get around this behaviour, or how other emulators deal with it.

Update: Some digging with this new information turned up this illuminating SO thread. I should be able to start making some progress again now!

! Wether this works also with _wrapped_ selections, depends on
!  - the terminal emulator:  Neither MIT X11R5/6 nor Suns openwin xterm
!    know about that.  Use the 'xfree xterm' or 'rxvt'.  Both compile on
!    all major platforms.
!  - It only works if xterm is wrapping the line itself
!    (not always really obvious for the user, though).
!  - Among the different vi's, vim actually supports this with a
!    clever and little hackish trick (see screen.c):
!
!    But before: vim inspects the _name_ of the value of TERM.
!    This must be similar to "xterm" (like "xterm-xfree86", which is
!    better than "xterm-color", btw, see his FAQ).
!    The terminfo entry _itself_ doesn't matter here
!    (e.g.: 'xterm' and 'vs100' are the same entry, but with
!     the latter it doesn't work).
!
!    If vim has to wrap a word, it appends a space at the first part,
!    this space will be wrapped by xterm.  Going on with writing, vim
!    in turn then positions the cursor again at the _beginning_ of this
!    next line. Thus, the space is not visible.  But xterm now believes
!    that the two lines are actually a single one--as xterm _has_ done
!    some wrapping also...

@mofux
Copy link
Contributor

mofux commented Apr 11, 2017

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 �[m�[H�[2J�[1;30Hqwertt�[2;1Hy its the �[2;1H that sets the cursor to row 2 column 1, which is where the extra character would be). I believe that we have to account the wrapped line as a real line, so when the cursor is set to row 2, it has to go to the wrapped line.

@LucianBuzzo
Copy link
Contributor Author

@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.
Oh well, at least I know how to progress with this feature now!

@parisk
Copy link
Contributor

parisk commented Apr 21, 2017

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.

@LucianBuzzo
Copy link
Contributor Author

@parisk I pushed some commits a few days ago fixing the vim issue and the scroll to bottom issue reported by @Tyriar
AFAICT We only need to sort the line wrap issue on the command prompt and we're there.
If you can give the branch a shakedown and see if anything is broken or showing weird behaviour that would be ace! 👍

* see https://jsperf.com/math-ceil-vs-bitwise
*
* @param {number} n - The value to round up.
*
Copy link
Member

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;
}
/**
Copy link
Member

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) {
Copy link
Member

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> {
Copy link
Member

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;
Copy link
Member

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);
Copy link
Member

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?

Copy link
Contributor Author

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.

@LucianBuzzo
Copy link
Contributor Author

I'm closing this PR in favour of #644

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants