fix(terminal+lsp): SHELL=$(POSIX-fallback) + skip selection-restore on empty buffer (v0.7.43)
All checks were successful
boundary-lint / PR boundary-claim (Lint (push) Has been skipped
boundary-lint / ban-list lint (Lint (push) Successful in 19s
boundary-lint / duplication-deadline (Layer 1/2) (push) Successful in 19s
Release Publish (Gitea session_helper) / verify-release-tag (push) Successful in 17s
Release Publish (Gitea session_helper) / publish-linux-x86_64 (push) Successful in 4m47s
ci / rust debug (push) Successful in 1m47s
ci / rust release (push) Successful in 2m24s
ci / test-health gate (push) Successful in 16s
ci / mutation test (broker) (push) Successful in 1m14s
ci / python (push) Successful in 1m41s

Two follow-ups to v0.7.42 user reports.

1. Terminal: ``zsh:1: permission denied:`` exit 126
---------------------------------------------------

v0.7.42 dropped the ``${SHELL:-/bin/sh}`` fallback assuming sshd
populates ``$SHELL`` in every login shell, but ``ssh -t host cmd``
runs the user's login shell with ``-c`` (NON-login mode); on some
remotes ``$SHELL`` is unset there, so ``exec "$SHELL" -il`` becomes
``exec "" -il`` → ``permission denied:`` exit 126.

Reinstate the fallback via POSIX ``if [ -z "$SHELL" ]; then
SHELL=/bin/sh; fi`` instead of ``${SHELL:-...}`` so the parser-bug
class that produced ``zsh:1: unknown exec flag -/`` in v0.7.31+ is
still avoided.

2. LSP: cross-file goto-def to unhydrated placeholder lands at (0,0)
--------------------------------------------------------------------

When LSP-pyright / rust-analyzer return a definition target whose
local cache copy is still a 0-byte placeholder, Sublime's
``window.open_file(path:42:5, ENCODED_POSITION)`` cannot place the
caret at row 42 col 5 — that row doesn't exist in an empty buffer —
and clamps to ``(0, 0)``. ``_apply_hydrate_result`` then captured
that ``(0, 0)`` selection before revert and restored it after,
overriding whatever position Sublime defers / re-applies once the
buffer has content. Net result: user lands at the file top instead
of the definition.

Skip capture/restore entirely when the pre-revert buffer was empty.
For the empty-pre-revert case the captured selection is always
``(0, 0)`` — restoring it can only override a Sublime-side deferred
placement, never recover the LSP target — so dropping the restore
is at least as good as before and lets any deferred ENCODED_POSITION
take effect.

The non-empty branch (e21b3a4 cross-file caret fix for already-
hydrated buffers) is unchanged.

Tests
-----

Python 1368 pass; Rust 486 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 00:23:55 +09:00
parent 5566e9ec16
commit dca8fb5a9c
6 changed files with 60 additions and 29 deletions

View File

@@ -1,6 +1,6 @@
[project]
name = "sessions-sublime"
version = "0.7.42"
version = "0.7.43"
description = "Sublime-facing Python code for Sessions."
requires-python = ">=3.8"
license = {text = "MIT"}

12
rust/Cargo.lock generated
View File

@@ -221,7 +221,7 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "local_bridge"
version = "0.7.42"
version = "0.7.43"
dependencies = [
"base64",
"glob",
@@ -432,7 +432,7 @@ dependencies = [
[[package]]
name = "session_helper"
version = "0.7.42"
version = "0.7.43"
dependencies = [
"base64",
"notify",
@@ -443,7 +443,7 @@ dependencies = [
[[package]]
name = "session_protocol"
version = "0.7.42"
version = "0.7.43"
dependencies = [
"base64",
"serde",
@@ -452,14 +452,14 @@ dependencies = [
[[package]]
name = "sessions_askpass"
version = "0.7.42"
version = "0.7.43"
dependencies = [
"tempfile",
]
[[package]]
name = "sessions_native"
version = "0.7.42"
version = "0.7.43"
dependencies = [
"base64",
"notify",
@@ -773,7 +773,7 @@ dependencies = [
[[package]]
name = "workspace_identity"
version = "0.7.42"
version = "0.7.43"
[[package]]
name = "zmij"

View File

@@ -12,7 +12,7 @@ resolver = "2"
[workspace.package]
edition = "2024"
license = "MIT"
version = "0.7.42"
version = "0.7.43"
authors = ["Myeongseon Choi <key262yek@gmail.com>"]
repository = "https://git.teahaven.kr/sublime-rs/sessions"
homepage = "https://git.teahaven.kr/sublime-rs/sessions"

View File

@@ -1677,19 +1677,36 @@ def _apply_hydrate_result(
# returns None / unexpected types) just let revert behave as
# it always has — better to lose the cursor than to crash the
# hydrate finish callback.
#
# Empty-pre-revert guard: when the LSP target was a 0-byte
# placeholder (cache stub never fetched), Sublime can't place
# the caret at row 42 col 5 — that row doesn't exist in an
# empty buffer — so the caret clamps to (0, 0) and our
# captured selection becomes ``[(0, 0)]``. Restoring that
# after revert overrides whatever position Sublime defers /
# re-applies once the buffer has content, leaving the user at
# the file top. Skip capture/restore entirely when the buffer
# had no content; downstream LSP-side caret placement (if
# any) wins, and we are no worse than landing at (0, 0).
view_size_fn = getattr(current, "size", None)
try:
buffer_size = view_size_fn() if callable(view_size_fn) else 0
except (RuntimeError, AttributeError, TypeError):
buffer_size = 0
captured_selections: List[Tuple[int, int]] = []
sel_fn = getattr(current, "sel", None)
if callable(sel_fn):
try:
regions = sel_fn()
if regions is not None:
for region in regions:
a = getattr(region, "a", None)
b = getattr(region, "b", None)
if isinstance(a, int) and isinstance(b, int):
captured_selections.append((a, b))
except (RuntimeError, AttributeError, TypeError):
captured_selections = []
if isinstance(buffer_size, int) and buffer_size > 0:
sel_fn = getattr(current, "sel", None)
if callable(sel_fn):
try:
regions = sel_fn()
if regions is not None:
for region in regions:
a = getattr(region, "a", None)
b = getattr(region, "b", None)
if isinstance(a, int) and isinstance(b, int):
captured_selections.append((a, b))
except (RuntimeError, AttributeError, TypeError):
captured_selections = []
run_command = getattr(current, "run_command", None)
if callable(run_command):
run_command("revert")
@@ -6957,11 +6974,21 @@ class SessionsOpenRemoteTerminalCommand(sublime_plugin.WindowCommand):
# form re-tripped some zsh setups in v0.7.31+ with
# ``zsh:1: unknown exec flag -/`` — the parameter expansion
# split such that the literal ``-/bin/sh`` reached ``exec`` as
# a flag instead of expanding to ``/bin/sh``. sshd populates
# ``$SHELL`` from the user's passwd entry in every login
# session, so the fallback was redundant; quoting ``"$SHELL"``
# also handles the rare path-with-spaces case.
remote_invocation = 'cd {}; exec "$SHELL" -il'.format(shlex.quote(remote_root))
# a flag instead of expanding to ``/bin/sh``.
#
# POSIX ``if [ -z "$SHELL" ]; then SHELL=/bin/sh; fi`` fallback
# not ``${SHELL:-...}``: ``ssh -t host cmd`` runs the user's
# login shell with ``-c`` (NON-login mode), so depending on the
# remote ``/etc/passwd`` and any ``.zshenv`` quirks ``$SHELL``
# may be unset. v0.7.42 dropped the fallback assuming sshd
# always populates it, but that broke users with non-standard
# shell configs (``zsh:1: permission denied:`` exit 126 from
# ``exec ""``). Reinstate the fallback in a form that avoids
# the ``:-`` parser bug entirely.
remote_invocation = (
'cd {}; if [ -z "$SHELL" ]; then SHELL=/bin/sh; fi; '
'exec "$SHELL" -il'.format(shlex.quote(remote_root))
)
# ``panel_name`` makes Terminus open the shell as a panel
# docked at the bottom of the active window. Without it
# Terminus defaults to a new tab in the editor pane group,

View File

@@ -452,13 +452,17 @@ def test_open_remote_terminal_opens_transient_terminus_pane(
# prefix was dropped — it confused interactive zsh on some macOS →
# Linux setups (``zsh: bad option: -/``). The ``${SHELL:-/bin/sh}``
# default form re-tripped the same class of zsh setups in v0.7.31+
# (``zsh:1: unknown exec flag -/``); ``"$SHELL"`` quoted is enough
# because sshd populates ``$SHELL`` from passwd in every login.
# (``zsh:1: unknown exec flag -/``). v0.7.42 dropped the fallback
# entirely on the assumption sshd populates ``$SHELL``; that broke
# users where ``ssh -t host cmd`` runs the login shell in non-login
# ``-c`` mode and ``$SHELL`` is empty (``permission denied:`` exit
# 126). v0.7.43 reinstates the fallback via POSIX ``if`` instead of
# ``:-`` so the parser-bug class is avoided.
assert args["cmd"] == [
"ssh",
"-t",
"prod",
'cd /srv/app; exec "$SHELL" -il',
'cd /srv/app; if [ -z "$SHELL" ]; then SHELL=/bin/sh; fi; exec "$SHELL" -il',
]
# ``auto_close=False`` so an unexpected shell exit (dotfile error,
# missing remote root, SSH drop) keeps the pane visible long enough

2
uv.lock generated
View File

@@ -854,7 +854,7 @@ wheels = [
[[package]]
name = "sessions-sublime"
version = "0.7.42"
version = "0.7.43"
source = { virtual = "." }
[package.dev-dependencies]