3 Commits

Author SHA1 Message Date
Vaclav Tvrdik
7b536a7efa Keep scrollback under a full-screen scroll region 2026-10-20 11:00:04 +02:00
Vaclav Tvrdik
40dac967e2 selection now without command modifier key 2026-10-16 14:34:50 +02:00
Vaclav Tvrdik
8f5564f4aa Keep render state on the Terminal; a plugin reload rebuilds the command
Sublime recreates every TextCommand instance whenever any plugin loads,
which dropped colored_lines and restarted the region key counter: old
overlays were never erased and keys were reused. RenderState now lives on
terminal.render_state, which survives reloads.
2026-10-12 15:39:29 +02:00
11 changed files with 199 additions and 104 deletions

View File

@@ -52,10 +52,11 @@ Generated at runtime, not in the repo: `Packages/User/vterm/vterm.hidden-color-s
`Char` carrying the extra `linefeed` field.
- **`SchemeManager`** (`scheme.py`) — module singleton `scheme_manager`. Owns the
generated scheme file and the mapping from colour combination to scope name.
- **`VtermRenderCommand`** (`render.py`) — Sublime keeps **one command instance per
view**, so its `colored_lines`, `_row_cache`, `_key_counter`, the scrollback chunk
bookkeeping (`_archived_chunks`, `_open_rows`, `_open_chunk`, `_trimmed`) and
`_lastrow` are that view's render state.
- **`VtermRenderCommand`** (`render.py`) — the per-frame logic. Sublime rebuilds every
`TextCommand` instance whenever any plugin loads, so the view's render state
(`RenderState`: `colored_lines`, `row_cache`, `key_counter`, the scrollback chunk
bookkeeping `archived_chunks`, `open_rows`, `open_chunk`, `trimmed`) lives on
`terminal.render_state`, bound as `self.state` at the top of each `run()`.
### Commands
@@ -74,10 +75,9 @@ Generated at runtime, not in the repo: `Packages/User/vterm/vterm.hidden-color-s
| `vterm_cleanup` | `VtermCleanupCommand` | renderer, on process exit |
| `vterm_close` | `VtermCloseCommand` | nothing — available for a user keybinding |
| `vterm_clear_undo_stack` / `vterm_noop` | `commands.py` | internal |
| `vterm_scroll` / `vterm_mouse_click` | `mouse.py` | mousemap, `drag_select` interception |
| `vterm_scroll` / `vterm_mouse_press` / `vterm_mouse_release` | `mouse.py` | mousemap |
Two application-wide `EventListener`s: `VtermCoreEventListener` (`listeners.py`) and
`VtermMouseEventListener` (`mouse.py`).
One application-wide `EventListener`: `VtermCoreEventListener` (`listeners.py`).
---

View File

@@ -11,6 +11,13 @@
"args": {"up": false},
"context": [{"key": "vterm_view.wheel_captured", "operand": true}]
},
{
// press reports the click, release reports the drag
"button": "button1",
"press_command": "vterm_mouse_press",
"command": "vterm_mouse_release",
"context": [{"key": "vterm_view.mouse_reporting", "operand": true}]
},
{
// press_command: sublime's own button3 binding is a column drag_select,
// which would clear the selection before a release command could read it

View File

@@ -79,10 +79,11 @@ or turns it off. Copy/paste use the normal Sublime keys, and pastes are
bracketed when the app asks for it. Copying re-joins soft-wrapped lines. The
middle button pastes the current selection straight into the app, X11-style,
without going near the clipboard; it does nothing when nothing is selected. The
wheel scrolls the view normally; while the app tracks the mouse the wheel and
left click go to it instead, and on the alternate screen without tracking each
wheel tick sends arrow keys (alternate scroll, as kitty/iTerm2 do — this is
what scrolls Claude Code's fullscreen UI before it enables mouse reporting).
wheel scrolls the view normally; while the app tracks the mouse the wheel, clicks
and drags go to it instead (Claude Code's own mouse selection and copy work as in
kitty), and on the alternate screen without tracking each wheel tick sends arrow
keys (alternate scroll, as kitty/iTerm2 do — this is what scrolls Claude Code's
fullscreen UI before it enables mouse reporting).
Sublime raises no wheel event at all for the trackpad (sublime_text#1838): set
a utility such as [LinearMouse](https://linearmouse.app/) to discrete scrolling
for Sublime Text and it arrives as wheel notches like any other. A notch sends the
@@ -149,15 +150,16 @@ terminal protocols — see `Architecture.md`.
into the view: an app that does not echo (vim in normal mode) would otherwise
leave the character behind. `on_modified` in `vterm/listeners.py` remains the
fallback for IME and composed input.
- `vterm/mouse.py` — SGR mouse reporting (wheel, click) when the app enables
- `vterm/mouse.py` — SGR mouse reporting (wheel, click, drag) when the app enables
tracking; native selection and scrolling otherwise. The trackpad is not
handled: it reaches no mousemap on macOS and is converted to wheel notches
outside the plugin.
Supported: alt screen (1049), synchronized output (2026), bracketed paste (2004),
mouse tracking 1000/1002/1003 + SGR 1006 (wheel and left click are reported; no
drag, no right button; the middle button pastes the selection instead of being
reported), focus events (1004),
mouse tracking 1000/1002/1003 + SGR 1006 (wheel and left button are reported; a
drag arrives as one motion at release, since Sublime raises none; no right
button; the middle button pastes the selection instead of being reported),
focus events (1004),
xterm modifyOtherKeys (`CSI > 4 ; n m`, for the modified keys with no
unambiguous legacy encoding: enter, tab, backspace, space), cursor styles (DECSCUSR —
block/bar and blink/steady via the view's caret settings; underline falls back

View File

@@ -26,8 +26,8 @@ from .vterm.commands import (
)
from .vterm.listeners import VtermCoreEventListener
from .vterm.mouse import (
VtermMouseClickCommand,
VtermMouseEventListener,
VtermMousePressCommand,
VtermMouseReleaseCommand,
VtermScrollCommand,
)
from .vterm.scheme import build_startup_schemes

View File

@@ -141,6 +141,11 @@ def run_case(char_by_char):
assert len(screen.history) == 1, (tag, len(screen.history))
assert screen.history[0][0].data == "a", tag
# an explicit full-screen scroll region still feeds scrollback
proc, screen, stream = make(10, 3)
feed(stream, "\x1b[1;3ra\r\nb\r\nc\r\nd", char_by_char)
assert len(screen.history) == 1, (tag, len(screen.history))
# ED 3 clears history and fires callback
cleared = []
proc = FakeProcess()

View File

@@ -9,9 +9,10 @@ from _bootstrap import FakeProcess
import sublime
from vterm.commands import VtermKeypressCommand
from vterm.emulator import VtScreen, VtStream
from vterm.mouse import VtermMouseClickCommand, VtermScrollCommand
from vterm.mouse import VtermMousePressCommand, VtermMouseReleaseCommand, VtermScrollCommand
from vterm.render import (
CHUNK,
RenderState,
VtermRenderCommand,
focus_cursor,
following,
@@ -171,6 +172,8 @@ def make_command(terminal):
# run() itself sets _scheme, but these tests call its helpers directly
command = VtermRenderCommand(terminal.view)
command._scheme = terminal.scheme
terminal.render_state = RenderState()
command.state = terminal.render_state
return command
@@ -282,7 +285,7 @@ assert terminal.view.text.startswith("RED..red..green"), repr(terminal.view.text
assert len(painted) == 2, painted
# keys are unique per view and recorded so decolorize_line can erase them
assert sorted(terminal.view.regions) == sorted(command.colored_lines[0]), \
assert sorted(terminal.view.regions) == sorted(command.state.colored_lines[0]), \
"colored_lines must track exactly the keys that were added"
command.decolorize_line(0)
assert terminal.view.regions == {}, "decolorize must erase every key it added"
@@ -290,6 +293,29 @@ assert terminal.view.regions == {}, "decolorize must erase every key it added"
print("colorize tests passed (arithmetic offsets, scope batching, key bookkeeping)")
# sublime rebuilds every TextCommand instance whenever any plugin loads; the
# bookkeeping lives on the terminal so a rebuilt command still erases what the
# old one painted, and its keys never collide with keys still in use
terminal, stream = make_terminal(rows=3, cols=20)
terminal.view = FakeView()
command = make_command(terminal)
stream.feed("\x1b[31mred line\x1b[0m\r\nplain")
command.update_lines(None, terminal)
terminal.screen.dirty.clear()
assert terminal.view.regions, "the red row must be painted"
# a reload: new command, same terminal
reloaded = VtermRenderCommand(terminal.view)
reloaded._scheme = terminal.scheme
reloaded.state = terminal.render_state
stream.feed("\x1b[H\x1b[2Kgone")
reloaded.update_lines(None, terminal)
assert terminal.view.regions == {}, \
"a rebuilt command must erase the old overlay: {}".format(terminal.view.regions)
assert reloaded.next_key() != "vterm#1", "keys must continue, not restart"
print("reload tests passed (render state survives a rebuilt command)")
# rows that leave the screen have their colours merged onto shared per-(scope,
# chunk) keys once the chunk closes; per-row keys would otherwise accumulate for
# the whole session, and merging every frame would re-send the whole chunk
@@ -318,10 +344,10 @@ command = make_command(terminal)
stream.feed("".join("\x1b[31mr{}\x1b[0m\r\n".format(i) for i in range(300)))
command.update_lines(None, terminal)
terminal.screen.dirty.clear()
assert set(command._archived_chunks) == {0}, command._archived_chunks
assert set(command.state.archived_chunks) == {0}, command.state.archived_chunks
command.trim_history(None, terminal)
assert "vterm@0@vterm.red.default" not in terminal.view.regions, "trimmed chunk key left behind"
assert 0 not in command._archived_chunks
assert 0 not in command.state.archived_chunks
assert "vterm@1@vterm.red.default" in terminal.view.regions, "boundary chunk must survive"
print("scrollback archive tests passed (chunk keys bounded, trim erases whole chunks)")
@@ -600,19 +626,31 @@ terminal.view.window_to_text = lambda xy: 0
window = FakeWindow(active=object())
terminal.view.window = lambda: window
stream.feed("\x1b[?1002h\x1b[?1006h")
click = VtermMouseClickCommand.__new__(VtermMouseClickCommand)
click.view = terminal.view
mouse_press = VtermMousePressCommand.__new__(VtermMousePressCommand)
mouse_press.view = terminal.view
mouse_release = VtermMouseReleaseCommand.__new__(VtermMouseReleaseCommand)
mouse_release.view = terminal.view
event = {"x": 0, "y": 0, "button": 1}
click.run(None, event)
mouse_press.run(None, event)
assert window.active_view() is terminal.view, "click did not focus the tab"
mouse_release.run(None, event)
assert terminal.process.written == "", repr(terminal.process.written)
# once focused, the same click reports as before
click.run(None, event)
# once focused, press and release at one cell report a click
mouse_press.run(None, event)
mouse_release.run(None, event)
assert terminal.process.written == "\x1b[<0;1;1M\x1b[<0;1;1m", repr(terminal.process.written)
print("click focus tests passed (first click focuses only, then reports)")
# a release elsewhere reports one drag motion first
terminal.process.writes.clear()
terminal.view.text = "r0 \nr1 \nr2 \nr3 \nr4 \n"
mouse_press.run(None, event)
terminal.view.window_to_text = lambda xy: terminal.view.text_point(2, 4)
mouse_release.run(None, event)
assert terminal.process.written == "\x1b[<0;1;1M\x1b[<32;5;3M\x1b[<0;5;3m", repr(terminal.process.written)
print("click tests passed (first click focuses only; press, drag and release report)")
# entering the alt screen folds the scrollback away (kitty hides it); leaving

View File

@@ -442,7 +442,9 @@ class VtScreen(pyte.Screen):
def index(self):
"""Line feed; at the bottom of the primary screen the top row goes to scrollback."""
if not self.alternate_buffer_mode and self.cursor.y == self.lines - 1 and self.margins is None:
# a region spanning the whole screen (CSI 1;<lines> r) is no region
full = self.margins is None or self.margins == (0, self.lines - 1)
if not self.alternate_buffer_mode and self.cursor.y == self.lines - 1 and full:
self.push_lines_into_history(1)
super().index()

View File

@@ -88,7 +88,7 @@ class VtermCoreEventListener(sublime_plugin.EventListener):
"""Answer the contexts the mousemap needs"""
# keymap contexts are native "setting.x"; mousemaps do not evaluate
# those the same way, so their flags still need the hook
if key not in ("vterm_view", "vterm_view.wheel_captured"):
if key not in ("vterm_view", "vterm_view.wheel_captured", "vterm_view.mouse_reporting"):
return None
value = view.settings().get(key, False)
if operator == sublime.OP_EQUAL:

View File

@@ -13,6 +13,8 @@ from .utils import settings
# SGR button numbers for the wheel; 64 is the "scroll" bit plus direction
WHEEL_UP = 64
WHEEL_DOWN = 65
# button 0 with the "moved while held" bit
MOTION = 32
def send_scroll(terminal, up, lines, cell=None):
@@ -36,7 +38,7 @@ def _event_to_cell(view, terminal, event, clamp=False):
"""Map a mouse event to 1-based (col, row) on the live screen, or None.
Clamping pulls a point above or below the screen onto the nearest row: the
wheel still belongs to the app there, a click does not."""
wheel and a release still belong to the app there, a press does not."""
pt = view.window_to_text((event["x"], event["y"]))
row, col = view.rowcol(pt)
row -= terminal.offset
@@ -48,46 +50,60 @@ def _event_to_cell(view, terminal, event, clamp=False):
return (min(col, screen.columns - 1) + 1, row + 1)
class VtermMouseEventListener(sublime_plugin.EventListener):
"""Hands button-1 clicks to the app, but only while it tracks the mouse."""
def on_text_command(self, view, name, args):
"""Replace drag_select with a mouse report; other click variants stay native."""
terminal = Terminal.from_id(view.id())
if not terminal:
return
if not terminal.screen.mouse_reporting_enabled():
# leave selection, caret and scrolling to sublime
return
if name == "drag_select" and args and args.get("event", {}).get("button") == 1 and len(args) == 1:
return ("vterm_mouse_click", {"event": args["event"]})
class VtermMouseClickCommand(sublime_plugin.TextCommand):
"""Reports a click to the app, or focuses the tab if it was not active."""
class VtermMousePressCommand(sublime_plugin.TextCommand):
"""Bound to button 1 in the mousemap while the app tracks the mouse."""
def run(self, edit, event):
"""Focus the tab on the first click, report press and release after that."""
"""Focus an inactive tab; else report the press and remember its cell."""
terminal = Terminal.from_id(self.view.id())
if not terminal:
return
terminal.mouse_press = None # a release may not come: the context can drop mid-drag
window = self.view.window()
if window and window.active_view() != self.view:
# we replaced drag_select, which is what focuses the group; this
# click only focuses, as clicking an unfocused terminal window does
# the mousemap binding displaces drag_select, which is what focuses
# the group; this click only focuses, as in a terminal window
window.focus_view(self.view)
return
cell = _event_to_cell(self.view, terminal, event)
if cell is None:
return
terminal.mouse_press = cell
terminal.send_mouse(0, cell[0], cell[1], pressed=True)
terminal.send_mouse(0, cell[0], cell[1], pressed=False)
# native drag_select only previews the selection while dragging; the
# app gets the real one at release
self.view.run_command("drag_select", {"event": event})
def want_event(self):
"""Ask sublime for the click coordinates."""
return True
class VtermMouseReleaseCommand(sublime_plugin.TextCommand):
"""Reports the release, after one drag motion when the button came up elsewhere.
Sublime raises no motion events, so a drag reaches the app as a single
move to the release cell; an app's selection then spans press to release.
The native preview selection goes, leaving the app's own highlight."""
def run(self, edit, event):
"""Report motion and release for the pending press, if any."""
terminal = Terminal.from_id(self.view.id())
if not terminal or terminal.mouse_press is None:
return
cell = _event_to_cell(self.view, terminal, event, clamp=True)
if cell != terminal.mouse_press:
terminal.send_mouse(MOTION, cell[0], cell[1], pressed=True)
terminal.send_mouse(0, cell[0], cell[1], pressed=False)
terminal.mouse_press = None
self.view.sel().clear()
self.view.run_command("vterm_show_cursor", {"scroll": False})
def want_event(self):
"""Ask sublime for the release coordinates."""
return True
class VtermScrollCommand(sublime_plugin.TextCommand):
"""Bound to scroll wheel in the mousemap while the app tracks the mouse or is on the alt screen."""

View File

@@ -116,34 +116,49 @@ def ensure_position(view, edit, row, col=0):
view.insert(edit, line_region.end(), " " * (col - lastcol))
class VtermRenderCommand(sublime_plugin.TextCommand):
"""Draws the screen into the view. Sublime keeps one instance per view, so
the caches below are that view's render state."""
class RenderState:
"""A view's render bookkeeping, kept on its Terminal.
def __init__(self, *args, **kwargs):
"""Set up this view's render caches."""
super().__init__(*args, **kwargs)
# the terminal's current SchemeManager; set at the top of each run()
self._scheme = None
Sublime rebuilds every TextCommand instance of every view whenever any
plugin module loads. State held on the command is lost then: the overlays
already in the view become untracked, so a row rewrite no longer erases
them, and the key counter restarts and hands out keys still in use.
"""
def __init__(self):
"""Empty bookkeeping, for a view that has nothing painted yet."""
# view row -> region keys painted on it
self.colored_lines = {}
# view row -> (segments, lf) as last rendered; the dirty set is only a
# repaint hint, so most dirty rows are identical frame to frame
self._row_cache = {}
self._key_counter = 0
self.row_cache = {}
self.key_counter = 0
# chunk ordinal -> scopes it holds a key for; ordinals are view row +
# rows trimmed so far, so they survive trim_history renumbering
self._archived_chunks = {}
self.archived_chunks = {}
# rows waiting for their chunk to close, and the chunk they belong to
self._open_rows = []
self._open_chunk = None
self._trimmed = 0
self.open_rows = []
self.open_chunk = None
self.trimmed = 0
class VtermRenderCommand(sublime_plugin.TextCommand):
"""Draws the screen into the view. Holds nothing a frame depends on: the
view's bookkeeping is `terminal.render_state` (see RenderState)."""
def __init__(self, *args, **kwargs):
"""Per-instance scratch only; the render state is bound in run()."""
super().__init__(*args, **kwargs)
# the terminal's current SchemeManager and RenderState; set at the top of each run()
self._scheme = None
self.state = None
self._lastrow = 0
self._applied_cursor_style = None
def next_key(self):
"""A region key unique to this view; per-row keys would collide after trim_history renumbers rows"""
self._key_counter += 1
return "vterm#{}".format(self._key_counter)
self.state.key_counter += 1
return "vterm#{}".format(self.state.key_counter)
def ensure_row(self, edit, row):
"""Row-existence check for the hot path, tracking the last row in python"""
@@ -158,6 +173,9 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
if not terminal:
return
self._scheme = terminal.scheme
if terminal.render_state is None:
terminal.render_state = RenderState()
self.state = terminal.render_state
screen = terminal.screen
# the caret and the tab name are the user's; an unfocused terminal leaves
@@ -256,16 +274,16 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
screen = terminal.screen
view.replace(edit, sublime.Region(0, view.size()), "")
for line in list(self.colored_lines):
for line in list(self.state.colored_lines):
self.decolorize_line(line)
for chunk, scopes in self._archived_chunks.items():
for chunk, scopes in self.state.archived_chunks.items():
for scope in scopes:
view.erase_regions(chunk_key(chunk, scope))
self._archived_chunks.clear()
self._open_rows = []
self._open_chunk = None
self._trimmed = 0
self._row_cache.clear()
self.state.archived_chunks.clear()
self.state.open_rows = []
self.state.open_chunk = None
self.state.trimmed = 0
self.state.row_cache.clear()
terminal.offset = 0
screen.history.clear()
if wipe_screen:
@@ -312,9 +330,9 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
# the dirty set is a repaint hint, not a diff: most dirty rows come
# back identical, and rewriting one costs an edit plus its region churn
rendered = (segments, lf)
if self._row_cache.get(line) == rendered:
if self.state.row_cache.get(line) == rendered:
return
self._row_cache[line] = rendered
self.state.row_cache[line] = rendered
by_scope = {}
needed = 0
@@ -343,7 +361,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
def colorize_line(self, line, line_start, by_scope):
"""Lay the colour overlays for one row, one add_regions per scope."""
view = self.view
keys = self.colored_lines.setdefault(line, []) if by_scope else None
keys = self.state.colored_lines.setdefault(line, []) if by_scope else None
for scope, spans in by_scope.items():
key = self.next_key()
view.add_regions(key, [sublime.Region(line_start + a, line_start + b) for a, b in spans], scope)
@@ -351,21 +369,21 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
def decolorize_line(self, line):
"""Drop a row's colour overlays before it is rewritten or erased."""
if line in self.colored_lines:
for key in self.colored_lines[line]:
if line in self.state.colored_lines:
for key in self.state.colored_lines[line]:
self.view.erase_regions(key)
del self.colored_lines[line]
del self.state.colored_lines[line]
def archive_rows(self, rows):
"""Queue rows that left the screen for their chunk's merge."""
for row in rows:
if row not in self.colored_lines:
if row not in self.state.colored_lines:
continue
chunk = (row + self._trimmed) // CHUNK
if chunk != self._open_chunk:
chunk = (row + self.state.trimmed) // CHUNK
if chunk != self.state.open_chunk:
self.flush_chunk()
self._open_chunk = chunk
self._open_rows.append(row)
self.state.open_chunk = chunk
self.state.open_rows.append(row)
def flush_chunk(self):
"""Merge the queued rows onto shared per-(scope, chunk) keys.
@@ -376,8 +394,8 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
whole set, so merging every frame would be quadratic in CHUNK.
"""
view = self.view
rows, chunk = self._open_rows, self._open_chunk
self._open_rows = []
rows, chunk = self.state.open_rows, self.state.open_chunk
self.state.open_rows = []
if not rows:
return
@@ -385,12 +403,12 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
stale = []
scope_of = self._scheme.scope
for row in rows:
keys = self.colored_lines.pop(row, None)
keys = self.state.colored_lines.pop(row, None)
if not keys:
continue
stale += keys
# the segments that were painted, straight from the row cache
segments = self._row_cache[row][0]
segments = self.state.row_cache[row][0]
start = view.line(view.text_point(row, 0)).begin()
for _, a, b, fg, bg, style in segments:
if is_default_run(fg, bg, style):
@@ -398,7 +416,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
scope = scope_of(fg, bg, style)
grouped.setdefault(scope, []).append(sublime.Region(start + a, start + b))
scopes = self._archived_chunks.setdefault(chunk, set())
scopes = self.state.archived_chunks.setdefault(chunk, set())
for scope, regions in grouped.items():
key = chunk_key(chunk, scope)
if scope in scopes:
@@ -431,7 +449,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
while row > cursor_row:
line_region = view.line(view.text_point(row, 0))
text = view.substr(line_region)
if len(text.strip()) == 0 and (row not in self.colored_lines or not self.colored_lines[row]):
if len(text.strip()) == 0 and (row not in self.state.colored_lines or not self.state.colored_lines[row]):
row = row - 1
else:
break
@@ -445,7 +463,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
trailing_region = sublime.Region(line_region.begin() + rev_wcwidth(text, cursor.x) + 1, line_region.end())
if not trailing_region.empty() and len(view.substr(trailing_region).strip()) == 0:
view.erase(edit, trailing_region)
self._row_cache.pop(row, None)
self.state.row_cache.pop(row, None)
def trim_history(self, edit, terminal):
"""Hold the view to scrollback_size rows, and drop rows below the screen."""
@@ -459,8 +477,8 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
m = max(lastrow + 1 - n, math.ceil(n / 10))
for line in range(m):
self.decolorize_line(line)
self.colored_lines = {k - m: v for (k, v) in self.colored_lines.items()}
self._row_cache = {k - m: v for (k, v) in self._row_cache.items() if k >= m}
self.state.colored_lines = {k - m: v for (k, v) in self.state.colored_lines.items()}
self.state.row_cache = {k - m: v for (k, v) in self.state.row_cache.items() if k >= m}
top_region = sublime.Region(0, view.full_line(view.text_point(m - 1, 0)).end())
view.erase(edit, top_region)
terminal.offset -= m
@@ -468,12 +486,12 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
# chunks wholly above the cut go; the boundary chunk drops the
# regions the erase collapsed to empty
self._trimmed += m
cut = self._trimmed // CHUNK
for chunk in [c for c in self._archived_chunks if c < cut]:
for scope in self._archived_chunks.pop(chunk):
self.state.trimmed += m
cut = self.state.trimmed // CHUNK
for chunk in [c for c in self.state.archived_chunks if c < cut]:
for scope in self.state.archived_chunks.pop(chunk):
view.erase_regions(chunk_key(chunk, scope))
for scope in self._archived_chunks.get(cut, ()):
for scope in self.state.archived_chunks.get(cut, ()):
key = chunk_key(cut, scope)
view.add_regions(key, [r for r in view.get_regions(key) if not r.empty()], scope)
@@ -482,7 +500,7 @@ class VtermRenderCommand(sublime_plugin.TextCommand):
for line in view.lines(tail_region):
row = view.rowcol(line.begin())[0]
self.decolorize_line(row)
self._row_cache.pop(row, None)
self.state.row_cache.pop(row, None)
view.erase(edit, tail_region)

View File

@@ -101,6 +101,7 @@ class Terminal:
"""State only; the pty and the threads come up in start()."""
self.view = view
self.scheme = scheme
self.render_state = None
# process.isalive() costs two waitpid syscalls; UI hooks read this instead
self.alive = True
self.default_title = ""
@@ -120,6 +121,8 @@ class Terminal:
# guards the queue/flag handoff between send_string and the drain thread
self._send_lock = threading.Lock()
self._wheel_captured = False
self._mouse_reporting = False
self.mouse_press = None # cell of the pending button-1 press, for the release report
self._scroll_pending = False
self.pending_clear = False
self.pending_clear_screen = False
@@ -328,7 +331,7 @@ class Terminal:
if frames >= UNDO_CLEAR_FRAMES:
frames = 0
self.view.run_command("vterm_clear_undo_stack")
self._sync_wheel_flag()
self._sync_mouse_flags()
if done[0] or not self.is_hosted():
LOG.debug("[renderer] stops")
break
@@ -424,12 +427,16 @@ class Terminal:
continue
self.process.write(string)
def _sync_wheel_flag(self):
"""Publish whether the app owns the wheel, for the mousemap context."""
captured = self.screen.mouse_reporting_enabled() or self.screen.alternate_buffer_mode
def _sync_mouse_flags(self):
"""Publish whether the app owns the wheel and the mouse, for the mousemap contexts."""
reporting = self.screen.mouse_reporting_enabled()
captured = reporting or self.screen.alternate_buffer_mode
if captured != self._wheel_captured:
self._wheel_captured = captured
self.view.settings().set("vterm_view.wheel_captured", captured)
if reporting != self._mouse_reporting:
self._mouse_reporting = reporting
self.view.settings().set("vterm_view.mouse_reporting", reporting)
def send_focus(self, focused):
"""Report focus gain or loss, when the app asked for it (?1004)."""