Skip to content

2026.2.1 - #109

Merged
hoxi merged 14 commits into
masterfrom
2026_2_1
Sep 10, 2026
Merged

hoxi merged 14 commits into
masterfrom
2026_2_1

Conversation

@hoxi

@hoxi hoxi commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

hoxi and others added 14 commits September 11, 2026 01:17
"Only Changes Since Common Ancestor" resolved its range by walking `git log` and
unioning every commit's changes, so files reverted inside the range stayed listed
and each path's base came from the oldest commit that touched it rather than the
merge base. Resolve `git merge-base <selection> HEAD` instead and feed it through
the same single-base diff the other scopes use, making a range scope exactly
`git diff <selection>...HEAD` -- the diff a pull request shows.

Range parsing lives in ScopeRefRange, free of platform types so it can be unit
tested. It accepts both dot forms but requires HEAD on the right, reporting
anything else as an invalid scope rather than silently reading it as "since the
common ancestor with HEAD". Also corrects the checkbox label, stops appending
"...HEAD" to refs that already carry a range, drops the now-unused
GitCommit.getChanges() reflection bridge, and runs :backend:test in CI.

Fixes #104

Co-authored-by: Ondrej Smola <ondrej.smola@gmail.com>
…ions

The RPC transport applies credit-based backpressure, so a slow frontend or
network legitimately stops the stream from being drained. The backend bridge
broke under exactly that condition: listener callbacks pushed payloads into a
callbackFlow via trySend, which silently discards events once the 64-element
buffer fills, and DataUpdated events are state-bearing with no retransmission, so
every discarded event was a file whose gutter never rendered. Restructure the
producer around conflation by file: listeners only mark a file dirty and wake the
emit loop, which reads the latest snapshot at send time and emits with a
suspending call, so intermediate states collapse into the newest one, pending
memory is bounded at one entry per file, and the final state always arrives.

On the frontend, supervise the subscription. durable only retries RPC-level
failures; two other terminations used to kill the gutter silently until IDE
restart, namely the backend answering with an empty flow because it does not know
the project yet, and any exception escaping event handling. Exceptions applying a
single event are now logged and skipped, and a completed or failed stream is
re-subscribed after a short delay.
…ates

Every DataUpdated message carried the full base and HEAD contents of the file,
JSON-encoded, although the ranges themselves are a handful of ints. The contents
rarely change -- typing, local-change recomputation and settings republishes all
produce new ranges against identical contents -- so on a slow link almost all of
the transferred bytes were repeats. Track per subscription which contents each
file was last sent with and omit them when unchanged; a contentsIncluded flag
distinguishes "unchanged, reuse your cache" from a genuinely absent headContent,
and the frontend falls back to the contents it already holds.

A new subscription starts with no memory and therefore always sends contents on
first contact, which keeps the durable re-subscribe after a connection drop
correct, and the memory is cleared together with AllCleared since the frontend
drops its cache at that point. Contents are still pushed rather than fetched on
demand because the frontend needs baseContent synchronously for live range
recomputation while typing. What remains per ordinary update is O(ranges) instead
of O(file size).
The plugin wrote 67 always-on statements to idea.log, worst on the gutter data
path, where a 200-file scope logged around 800 lines every time the scope
changed. Demote all 11 INFO calls to DEBUG; demote 11 of the 12 LOG.error calls,
since Logger.error feeds the platform's error-report mechanism and raised an "IDE
internal error" for conditions the plugin already recovers from, the one kept
being VcsTree's fallback failure, which does mean the tool window is broken with
no recovery left; and demote 16 WARN calls that fire on normal outcomes such as
teardown races and tabs the user cannot move. Debug calls whose argument
construction is not free on hot paths are now guarded, and everything stays
reachable through the existing single #gitscope switch.

With the noise gone, add the debug coverage the refresh pipeline was missing, so
a scope that stops picking up filesystem changes can be diagnosed from the log
instead of guessed at: MyBulkFileListener now reports the VFS batch size and
which project it refreshes, ViewService.collectChanges logs the two early exits
that abandon a refresh without scheduling anything, ChangesService logs
collections abandoned or interrupted by a newer generation, an empty repository
list, cache hits, final counts and unresolvable scopes and now warns from
onThrowable, and MyLineStatusTrackerImpl logs updates skipped, superseded or
matching no open editor. Steady-state output is still zero lines; 31 WARN and 1
ERROR remain for genuine failures.
Three compounding races left the Git Scope window showing pre-git-operation
state, with conflict files staying red after a resolved rebase as the reported
symptom. The window is a few hundred milliseconds wide and needs a file to be
opened inside it, which is why it reproduced on real branches but never in
minimal tests: a collection during the conflict state caches red results, then
finishing the rebase schedules a fresh collection, but opening any file bumped
the apply generation, causing that fresh collection to abandon before its run()
reached the cache clear, and scheduled a cache-permitted collection that served
the conflict-era entries under the now-current generation.

One fix per link in the chain. fileOpened no longer bumps the apply generation,
since opening a file changes no scope input and the bump only discarded whichever
fresh collection was in flight; the call remains as a cache-warmer. The cache is
cleared when a fresh collection is scheduled rather than inside its run(), so
pre-operation entries cannot outlive the operation even if that collection is
abandoned. And the collection callback is now always invoked exactly once, with
null when superseded or cancelled, so chained UI work such as the file-colors
refresh after a tab switch is never silently lost.

Refs #78
GitScopeFileStatusProvider answers from the current scope map, but the platform
caches those answers until fileStatusesChanged() is called, which only tab
switches, boot and the settings dialog did. A collection that changed the scope,
such as a file leaving it after a rebase or a status changing after a commit,
therefore updated the tool window but left Project-view and editor-tab colors
showing the previous scope until the user switched tabs.

Refresh the colors when an applied collection materially changed the scope map,
compared as path -> FileStatus. The comparison guards the common no-change apply,
because the refresh goes through FileStatusManager.fileStatusesChanged(), which
can disturb line status trackers and is not free.

Refs #78
MyBulkFileListener re-collected every open project on every VFS batch anywhere:
build outputs, caches, files of unrelated projects. Beyond the wasted git work,
each spurious collection cancels whichever collection is currently in flight, so
busy phases such as a rebase writing dozens of files kept restarting the very
collections that were about to deliver fresh state, widening the race windows
behind issue #78.

Skip projects where no event path lies under any of their git repository roots.
Everything under a repository root still triggers, including .git internals, so
no existing refresh source is lost; repositories not being registered yet also
skips, since there is nothing to collect against until the VCS mapping listener
fires.

Refs #78
Rename Tab, Reset Tab Name and Move Tab Left/Right were all absent from the tool
window's tab context menu over Remote Development. The cause was registration,
not logic: the actions were declared in gitscope.backend.xml, so they existed
only in the host's action registry, while in split mode the tab strip is rendered
by the frontend and its context menu is built from the frontend registry. Register
the four actions once in the frontend module for both modes -- monolith loads
gitscope.frontend too, and UtilRpcApi resolves there as well -- so a single
registration serves both modes with no second implementation to keep in step.
This also removes three copies of a helper that read ContentTabLabel.myContent
via getDeclaredField, which does not search superclasses and so already hid the
actions in monolith; ToolWindowContextMenuActionBase replaces it.

The tool window, its ContentManager and the scope models are backend state, so
the actions decide enablement from what the tab strip shows and delegate over
UtilRpcApi to a new TabActionService, which owns the rules and re-validates every
request. Reset Tab Name additionally needs to know whether a tab carries a custom
name, which is model state the frontend cannot see, so the backend publishes the
custom-named tab indices as a StateFlow, republished after every rename, reset
and move and once when a frontend subscribes. The frontend mirrors it in a
service the action reads synchronously, since update() cannot suspend. Unknown
state counts as enabled, so the worst case is an action that runs and finds
nothing to reset, never one that is missing.
The tooltip on a renamed tab was missing right after boot in monolith, and always
missing in split mode, for two independent reasons. At boot it is built from the
tab's branch-based name, which resolves to an empty string before the VCS mapping
is ready, and nothing retried afterwards, so a tab restored with a custom name
kept no tooltip for the rest of the session. In split mode the tab strip is
rendered against the frontend's Content objects while setDescription is called on
the backend's copy, which never reaches the label.

Both are fixed by making the original names part of the state the backend already
pushes for tab actions: the flow now carries a map of tab index to branch-based
name rather than a list of renamed indices, and the frontend applies it as the
tooltip on its own contents. Reset Tab Name's enablement comes from the same map,
since presence in it means the tab has a custom name. The map is republished
after every rename, reset and move, when a frontend subscribes, after the tabs
are restored, and after each change collection, the last being what finally
supplies names that were unresolvable at boot. Tabs whose branch name is still
unresolvable are left out rather than published with a blank tooltip.
The platform finishes a tab drag by re-adding the content at its new index, which
reaches the plugin as a content removal indistinguishable from closing a tab.
MyTabContentListener answered it by removing the model, so the dragged tab lost
its scope, the collection order no longer matched the tab order, and save()
persisted the result, which is why the tab was gone after restarting the IDE.
Only monolith is affected, since the platform gates tab dragging on
AppMode.isMonolith().

There is no "content moved" event to listen for, so repair rather than prevent.
The removal still happens exactly as before, since that path also runs when tabs
are torn down in bulk on project close, but the removed Content is remembered,
and if the very same Content is added back, contentAdded recognises the move and
rebuildCollectionFromTabOrder() derives the collection from the tabs themselves.
Detection is deliberately event-driven rather than timed, because the platform
can complete a drag several EDT ticks after the removal; the remembered contents
are held weakly, so a Content that really was closed is collected without further
bookkeeping. HEAD is moved back to the front and "+" back to the end before the
collection is rebuilt, and the renamed-tab map is republished because the indices
it addresses have shifted.
Next/Previous Change and Next/Previous Changed File visit the changed files in
the order the Git Scope tree shows them, read from the tree itself so that
whichever grouping is active -- module, repository or directory -- is followed.
When the tool window has not built a tree yet, files are ordered hierarchically
instead: into a subdirectory before the files beside it, names compared
case-insensitively with digit runs read as numbers.

Entries that cannot hold a caret are left out of the order rather than failing
when opened: files deleted in the scope, directories recorded as changes such as
submodules, and binary files. Each opened file is logged with its file type under
#gitscope.
Next/Previous Change and Next/Previous Changed File asked for the opened editor
to be focused. When navigation is driven from the tool window that hands the
focus away, and Project View following the file leaves it there, so the next
keystroke no longer reaches Git Scope. The editor is now focused only when
navigation started from the editor; the caret position it would have reported is
already tracked as the last navigated position.

The changes tree keeps painting its selection as focused, so the file navigation
moved to stays visible in the tool window while the editor holds the focus.
Adds the changelog section for the release. The common-ancestor fix moves from
the 2026.2 section, which was already published without it.
ChangeListManager restores the changelist persisted in workspace.xml and then
only updates incrementally, over whatever VcsDirtyScopeManager reports as dirty.
An entry whose file no longer exists can never enter that dirty scope, so it
survives every update, is re-persisted on close and returns on the next open.
Restarting does not clear it; the scope kept showing files git reports as clean
until an unrelated commit touched .git/index and forced a rescan.

Mark everything dirty once per project on VcsMappingListener readiness, collect
behind ChangeListManager.invokeAfterUpdate, drop changelist entries whose file no
longer exists (DELETED exempt), and route untracked files through the same
repository and staleness checks.
@hoxi hoxi added this to the 2026.2.1 milestone Sep 10, 2026
@hoxi hoxi self-assigned this Sep 10, 2026
@hoxi
hoxi merged commit 2e396cd into master Sep 10, 2026
1 check passed
@hoxi
hoxi deleted the 2026_2_1 branch September 10, 2026 23:29
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.

1 participant