Skip to content

Share language servers, make shortcuts configurable, and finish the editor's open gaps - #248

Merged
JE-Chen merged 16 commits into
mainfrom
dev
Jul 26, 2026
Merged

Share language servers, make shortcuts configurable, and finish the editor's open gaps#248
JE-Chen merged 16 commits into
mainfrom
dev

Conversation

@JE-Chen

@JE-Chen JE-Chen commented Jul 26, 2026

Copy link
Copy Markdown
Member

What this adds

Fifteen commits working through the editor's remaining gaps, plus the defects
found while doing so.

Architecture

  • One language server, shared. Each editor started a server of its own, so
    five open TypeScript files meant five processes each indexing the whole
    project. A session now owns the process, keyed by command and project root,
    while the per-file client keeps only its own version number and requests.
    Replies go back to whichever tab asked, diagnostics to the tab holding that
    URI, and the server stops when its last tab closes.
  • Shortcuts can be changed. Every key sequence was written down where it was
    used, so nothing could be reassigned without editing the source. They live in
    one table now, read by the menus, the toolbar and the editor alike, with a
    dialog under Style that refuses to save while two commands claim the same keys.
  • The status bar describes the tab in front of you. It showed the encoding
    from the global setting and worked out the line ending by re-reading up to 8KB
    from disk on every tab change -- so a choice made in the Encoding or Line
    Endings menu never appeared, and file I/O ran on the UI thread. A language
    indicator sits alongside them.

Editing

Carets that hold their own selections (Shift with any arrow key extends them
all), vertical movement and Home/End for multiple carets, snippet placeholders
that only need typing once, brace-based folding for the C-family languages, and
minimap marks that follow the search rather than the word under the caret.

Language server

Signature help, find references, quick fixes and document symbols. Only actions
carrying edits are offered: a command-only action needs another round trip, and
one that does nothing when pressed is worse than none.

Git

Unstaging, committing the index, stash save/list/pop, and listing and resolving
conflicts. Staging a hunk had no counterpart, so putting the wrong one in left
no way back short of the command line.

Testing and appearance

Per-failure tracebacks and a coverage total in the test panel, and a light set
of the editor's own colours -- everything it paints itself was tuned against a
dark background, so a light style left the minimap dark and the guides invisible.

Defects found along the way

  • The suite aborted the interpreter on Python 3.10, roughly half of all runs,
    at whichever test next spun an event loop. Behind it was QThread: Destroyed while thread is still running, which Qt reports through qFatal: no
    traceback, and a crash site far from the cause. Closing an editor stopped its
    workers but left the debounce timers running, and a timer firing afterwards
    started a lint thread nothing was left to wait for.
  • Seven key sequences had two owners. Qt runs neither of two claimants, so
    each clash quietly disabled both features -- Ctrl+D stopped duplicating
    lines and F5/F9 stopped reaching the toolbar.
  • Multiple carets lost their alignment on one arrow press: the primary caret
    stayed put while the extras moved.
  • resolve_conflict succeeded on files with no conflict, where all
    checkout --ours does is quietly stage a file nobody asked to stage.
  • Quick fixes sent the wrong diagnostics -- the editor's converted form
    rather than the server's own objects, which do not even count lines the same
    way.
  • UnicodeDecodeError sat after ValueError in two readers, making the
    decode branch unreachable.

Performance

The generic highlighter ran one regular expression per keyword over every line
-- 44 passes per line for TypeScript. As a single alternation that measures 72
microseconds per line down to 17.5, so a 20,000-line file highlights in 0.37s
rather than 1.52s.

Verification

ruff check . clean. 1669 tests pass, run three times over on 3.11 and three
times on 3.10 -- the version and platform where the crash above appeared.

JE-Chen added 15 commits July 26, 2026 14:19
A split view shows two editors of the same document at once. Their actions
shared the default window-level context, which gave every editor sequence two
owners in that window -- and Qt fires neither of two owners, so the whole set
went dead as soon as the view was split. Tabs were never affected, since Qt
skips invisible widgets when matching.

This was tried once before and reverted: the suite was crashing at the time
and the change looked responsible. The crash was the lint thread outliving its
editor, fixed separately; with that gone, six full runs on 3.10 are clean.
It showed the encoding from the global setting rather than the one the tab was
opened or saved with, and worked out the line ending by re-reading up to 8KB of
the file from disk on every tab change. Both meant a choice made from the
Encoding or Line Endings menu never reached the display, and the second put
file I/O on the UI thread.

Both now read the tab's own state, the menus ask for a refresh after changing
it, and a language indicator sits alongside them -- twelve languages are
highlighted and until now nothing said which one was in effect.
Breakpoints only reached pdb when the debugger started, so a line marked or
unmarked part-way through a session had no effect until the next run. Toggling
one now sends the matching break or clear command, which pdb applies at its
next prompt -- exactly when it should take effect.
A number repeated in a snippet body was only a tab stop the first time; the
rest expanded to plain text, so "${1:name} = ${1:name}" meant typing the same
name twice and nothing kept the two in step.

Repeats are now recorded as mirrors of their stop and rewritten as you type in
it. They are tracked as positions and a length rather than cursors, because a
cursor selection collapses when its whole content is replaced -- which is
exactly what the first keystroke over a placeholder does.
The middle column of marks showed occurrences of the word under the caret even
while a search was open, so the one thing the user was actually looking for was
the one thing it did not show. It now marks the search hits whenever something
is being searched for, and falls back to the word under the caret otherwise.

Search matching is on the typed string rather than whole identifiers, so "val"
marks the line holding "value" -- which is what a search is for.
Folding was indentation-only, which suits Python but misreads the C-family
languages now that they are highlighted: a brace placed on its own line left
the header no more indented than its body, so the block did not fold at all.

Those languages now fold on brace pairs, with the closing brace left visible.
Braces inside strings and comments are skipped, since one in a string would
throw every pair after it out of step.
The extra carets only went left and right, so any vertical move meant losing
them and starting over. They now follow Up, Down, Home and End as well, each
keeping its own column and stopping at a shorter line''s end.

Left and right also take the primary caret along. It used to stay put while the
extras moved, so a single arrow press pulled them out of alignment -- the one
thing multiple carets exist to preserve.
Extra carets were positions with nothing behind them, so Shift with an arrow key
did nothing and there was no way to select the same span on several lines at
once. Each caret now keeps an anchor, Shift extends them all together, and the
spans are washed in the caret colour.

Insert, Backspace and Delete go through one operation -- replace each caret''s
range with this text -- rather than three sets of offset arithmetic. A caret
without a selection covers a zero-length range, so inserting and replacing are
the same thing, and the positions afterwards fall out of one calculation.
Everything the editor paints itself -- line numbers, indent guides, change
markers, the minimap, syntax -- had one set of colours, tuned against a dark
background. Choosing a light qt-material style left the minimap dark, the
guides invisible and the syntax washed out.

There are now two sets, and picking a style moves the colours with it. A colour
the user chose stays: the test is whether a value is still one of the known
defaults, since the settings file writes every colour back and cannot say which
ones were deliberate.
The generic highlighter held one regular expression per keyword and ran every
one of them over every line -- 44 passes per line for TypeScript. They are now
a single alternation, which measures 72 microseconds per line down to 17.5, so
a 20,000-line file highlights in 0.37s rather than 1.52s.
The panel listed outcomes and jumped to a line, but the reason lived only in
the terminal it never showed. It now runs with --tb=short, keeps each failure''s
traceback, and shows the selected test''s in a pane below the list.

A coverage box adds --cov and reports the total beside the summary. It needs
pytest-cov in the project being tested rather than in the editor, so leaving it
off costs nothing and turning it on says plainly when the plugin is missing.
Every sequence was written down where it was used -- one in each menu builder,
the toolbar and the editor -- so nothing could be reassigned without editing the
source, and a clash only showed up as a key that did nothing.

They now live in one table keyed by command, and everything reads its keys from
there. A dialog under Style lists every command, refuses to save while two of
them claim the same keys, and records only what differs from a default so a
later change to a default still reaches the user.

Ctrl+Shift+Escape is spelled Ctrl+Shift+Esc to match what Qt itself produces;
otherwise the first save recorded that command as changed without anyone
touching it.
Each editor started a server of its own, so five open TypeScript files meant
five processes each indexing the whole project -- five times the memory and
startup cost, with five separate ideas of the same code.

A session now owns the process, keyed by command and project root, and the
per-file client keeps only its own version number and requests. Replies go back
to whichever tab asked and diagnostics to the tab holding that URI. The server
shuts down when the last of its tabs closes, which is also where didClose is
sent; saving sends didSave, without which some servers never run their slower
checks at all.
Signature help, find references, quick fixes and document symbols were the
parts of the protocol still unused. Signature help and references sit beside
hover in the right-click menu, and a quick fix applies the edits the server
offers.

Only actions that carry edits are listed: a command-only action needs another
round trip, and offering one that does nothing when pressed is worse than not
offering it. The diagnostics sent with a fix request are the server''s own
objects rather than the editor''s converted form, which does not count lines the
same way.
Hunks could be staged but never unstaged, so putting the wrong one in left no
way back except the command line, and nothing committed the index -- which is
the whole point of staging hunk by hunk.

Unstaging returns the index to the committed content and leaves the working tree
alone; a file that was never committed is dropped from the index instead.
Committing commits the index, so the unstaged edits stay where they are. Both
sit in the right-click menu beside staging.

Stash save, list and pop, and listing and resolving conflicts, round out the git
service. Resolving refuses a file that is not in conflict: git accepts
"checkout --ours" there quite happily, but all it does is quietly stage a file
nobody asked to stage.
@codacy-production

codacy-production Bot commented Jul 26, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 672 complexity · 12 duplication

Metric Results
Complexity 672
Duplication 12

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

- The traceback parser matched banners with a regex whose lazy middle sits
  between two repeated fences, which backtracks exponentially on a line of
  nothing but fence characters -- and pytest prints plenty of those. It scans
  instead.
- Saving a shortcut called a reload method that was never written, so the
  change only reached tabs opened afterwards and never the menus or toolbar at
  all. Actions are bound through one place now and all of them follow a save.
- handle_message grew past the complexity limit; each reply is the same shape,
  so one table dispatches them.
- Resolving a conflict, unstaging and the exception tests each had a smaller
  fix: an unnecessary list(), an assertion comparing a call with itself, one
  with its arguments the wrong way round, and two raises blocks holding two
  calls apiece.
@sonarqubecloud

Copy link
Copy Markdown

@JE-Chen
JE-Chen merged commit 0c143fd into main Jul 26, 2026
9 checks passed
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