Skip to content

Refactor so isWrapped isn't public writeable - #5788

Open
PerBothner wants to merge 62 commits into
xtermjs:masterfrom
PerBothner:setWrapped
Open

PerBothner wants to merge 62 commits into
xtermjs:masterfrom
PerBothner:setWrapped

Conversation

@PerBothner

Copy link
Copy Markdown
Contributor

Having a publicly-writable isWrapped property in BufferLine restricts our implementation options for how wrapped lines are represented in a Buffer. For example, it conflicts with the proposal in #5673.

To hide this implementation choice, this PR makes isWrapped read-only, and adds a Buffer.setWrapped method (taking an absolute row number) for modifying the isWrapped state. There is also an internal-only BufferLine.setWrapped method used to implement Buffer.setWrapped and in one other place.

This gives greater flexibility in how BufferLine and line-wrapping are implemented.

This is likely to trivially conflict with PR #5785. If one of these is merged, I'll update the other one to fix any conflicts.

Instead to change isWrapped state use a Buffer.setWrapped method.
This allows for potential flexibility in how BufferLine
and line-wrapping are implemented.
@PerBothner

Copy link
Copy Markdown
Contributor Author

It would be great if someone could merge this. It allows (but does not require) alternative implementation stategies for BufferLine, such as PR #5797. I believe this should be trivially reviewable and hopefully non-controversial.

@jerch

jerch commented Apr 26, 2026 •

Copy link
Copy Markdown
Member

This gives greater flexibility in how BufferLine and line-wrapping are implemented.

Can you expand on why this would be needed? Is this an attempt to hide the row logic behind Buffer.* access (which is prolly needed for your logical line implementation)? (Btw our isWrapped logic is flawed currently, we hold this flag on the wrong line leading to all sorts of forward/backward lookups to correct the behavior.)

If we go that route with hiding abstraction behind Buffer.* access, then this also would apply for getters and prolly data getters/setters. The latter smells like a performance grave to me, getter/setter interfaces are always slower than direct access (with property getter/setter being even worse). I suggest to discuss this route first and to back it up with some perf measures.

@PerBothner

Copy link
Copy Markdown
Contributor Author

The motivation of this PR was to split off part of PR #5797 that can be reviewed separately. Like ad hoc "stacked PRs".

For #5797 (and probably other alternative Buffer implementation strategies) changing a line from wrapped to non-wrapped or vice versa isn't a matter of just flipping a bit - there are more extensive changes that need to be done. For PR #5797 you would need to split or merge the related LogicalLine objects. So changing the isWrapped state becomes a more expensive operation. (Though note that setting isWrapped is usually when a new line is scrolled in, when a new BufferLine is created. Clearing isWrapped is usually when a line or buffer is erased, so those operations can be combined.)

Is this an attempt to hide the row logic behind Buffer.* access (which is prolly needed for your logical line implementation)?

Most of the other changes involving LogicalLine are hidden inside BufferLine.js (except for buffer reflow in Buffer.ts, which becomes vastly simpler and more efficient), but changing isWrapped to read-only is the main API change.

It would probably be straightforward to make isWrapped is a readonly property (rather than a getter), but I haven't looked into that. Either way, I would be astounded if the difference from direct access is measureable, as isWrapped/setWrapped are not called on a per-character basis.

Btw our isWrapped logic is flawed currently, we hold this flag on the wrong line leading to all sorts of forward/backward lookups to correct the behavior.)

PR #5797 makes it trivial to see if the current line wraps to the next one: Just check line.nextBufferLine.

Perhaps it would be more useful to come back this PR after looking at PR #5797. If you prefer, I can merge this into the latter, making the latter slightly larger, but perhaps better justified.

I am also interested in exploring this Marker proposal, which I think will have significant performance and extensibility advantages, but I think PR #5797 needs to be merged first.

dependabot Bot and others added 22 commits May 2, 2026 16:48
Bumps [axios](https://github.com/axios/axios) from 1.13.2 to 1.15.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](axios/axios@v1.13.2...v1.15.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Disable desynchronized 2D canvas mode for image layers so resize paint stays synchronized and avoids blank-frame jitter on some platforms.

Signed-off-by: sergiochan <yuheng9211@qq.com>
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.11 to 1.16.0.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](follow-redirects/follow-redirects@v1.15.11...v1.16.0)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-version: 1.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
- change interface to IFunctionIdentifier (API change)
- extend parser states APC_ENTRY, APC_INTERMEDIATE, APC_PASSTHROUGH
- remove APC_STRING
- change ApcParser.start to retrieve ident
- change actions APC_START and APC_PUT
@PerBothner

Copy link
Copy Markdown
Contributor Author

This is part of the large PR #6160.

@jerch jerch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

I have a few remarks below, but nothing blocking. Plz let me know, if you want to address some of them, otherwise this looks ready to merge.

Comment thread src/common/buffer/Buffer.ts Outdated

public setWrapped(absrow: number, value: boolean): void {
const line = this.lines.get(absrow);
line instanceof BufferLine && line.setWrapped(value);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The && invocation is an interesting pattern here, I guess you did it for speed reasons. I also found && to be much faster on v8 compared to ordinary branching.

Minor perf suggestion - if the instanceof check is not needed (because .get() will always return either something nullish or a bufferline), then we should prolly just test for truthiness.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC I used the X && Y pattern because lint would complain about if (X) Y, though it did allow if (X) {Y}, which I though a bit inelegant. I actually think if (X) Y is slightly for readable - but if X && Y is faster we might as well keep it.

The instanceof check is because IBufferLine specifically does not provide setWrapped, which is supposed to used only to implement Buffer.setWrapped. I made a change to get rid of BufferLine.setWrapped completely - instead you have to do (line as any)._isWrapped. That should make it clear(er) you're mucking about with internals.

Comment thread src/common/services/BufferService.ts Outdated
this._cachedBlankLine = newLine;
}
newLine.isWrapped = isWrapped;
(newLine as BufferLine).setWrapped(isWrapped);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have an interface issue here? Isn't buffer.getBlankLine meant to always return a bufferline?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above. Replaced by (newLine as any)._isWrapped = isWrapped.

Note the assignment is only needed for the case we don't call getBlankLine, so I put it in an else branch..

);
if (clearWrap) {
line.isWrapped = false;
this._activeBuffer.setWrapped(this._activeBuffer.ybase + y, false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for using absolute addressing here now, why not on the line?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand the question. Buffer.setWrapped takes an absolute line number. We don't want to use BufferLine.setWrapped (now removed) because it is internal.

Note one reason for removing BufferLine.setWrapped (which is the same reason for making BufferLine.isWrapped read-only) is because some implementations (including the proposed LogicalLine implementation) need more state, and modifying isWrapped can't be done without access to at least the previous line.

line.fill(this._activeBuffer.getNullCell(this._eraseAttrData()), respectProtect);
this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y);
line.isWrapped = false;
this._activeBuffer.setWrapped(this._activeBuffer.ybase + y, false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as above - why the absolute addressing now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reply.

if (nextLine) {
nextLine.isWrapped = false;
}
this._activeBuffer.setWrapped(j + 1, false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These n+1 lookups for wrapped state are really annoying. I think I already addressed it in some comment elsewhere - we basically have a slightly off handling of wrapped state:
We set the flag on the next line to indicate, that it continues from the previous one. This introduces a lot of nasty edge cases, where we have to fix wrapped on n+1.
A more logical place would be to store it on the n-th line to indicate, that it continues into the next line. This plays much nicer with left-right and top-down progressing of the default cursor movement.

But that is a major rewrite and we are in a state, where our more complicated abstraction works "good enough" - haha.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, the LogicalLine re-write makes it trivial to implement a isWrapping (read-only) property, which is true when a line continues into the next line:

public get isWrapping(): boolean {
    return !!this.nextBufferLine;
}

Comment thread src/common/WindowsMode.ts
if (nextLine && lastChar) {
nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE);
if (lastChar) {
bufferService.buffer.setWrapped(bufferService.buffer.ybase + bufferService.buffer.y, lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a really hackish assumption over data state and def. will wrongly merge lines, that happened to end on the last cell and had a hard break following. (no critic on your code, I see that you just transformed it)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I haven't really thought about this, but I followed the existing logic.

Though only for internal use (and only used twice), it could
lead to improper use, even if not available in IBufferLine.
Instead, change _isWrapped directly after a cast to any - that
makes it more obvious we're tweaking internals.

@jerch jerch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plz see my comments below.

Comment thread src/common/buffer/Buffer.ts Outdated
Comment on lines +129 to +130
const line = this.lines.get(absrow);
line instanceof BufferLine && line.setWrapped(value);
(line as any)._isWrapped = value;

@jerch jerch Oct 12, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks now like it will throw, if get() returns undefined? I think the truthiness check will be needed, as get may return nothing.

Also the as any is not good, as will swallow lit. anything. Imho it is better to add _isWrapped to the interface type the ring buffer returns. This makes the later rework bark loud, once that flag vanishes - so we will miss places, where is needs to be fixed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the truthiness check, and avoided the as any. Instead, make _isWrapped public in BufferLine (but not IBufferLine), with some wrong warnings.

Comment thread src/common/services/BufferService.ts Outdated
newLine = buffer.getBlankLine(eraseAttr, isWrapped);
this._cachedBlankLine = newLine;
} else {
(newLine as any)._isWrapped = isWrapped;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise.

Instead, make _isWrapped public in BufferLine - but add strong warnings.
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.

5 participants