Conversation
Groundwork for showing in the editor what changed since the last commit. line_status compares a buffer against a baseline with difflib and reports which lines were added, modified, or sit just below a deletion, plus the navigation helpers for stepping between them. It is pure logic: no Qt, no git, no I/O, and it gives up on very large buffers rather than stalling the edit it is meant to annotate. file_baseline reads the HEAD version of a path through GitPython, finding the repository from the file itself. Line endings are normalised to \n so the comparison is unaffected by how the repository stores them; a file outside a repository, never committed, or not utf-8 text simply has no baseline.
The gutter now shows how the open file differs from its last commit: a green bar for an added line, orange for a modified one, and a thin red line where lines were deleted. Ctrl+Alt+Down and Ctrl+Alt+Up step through the changes, wrapping around. The baseline is read on a background thread when a file is opened, since that is git and file I/O, and a read left in flight when the file changes is discarded rather than applied to the wrong buffer. Recomputing against that baseline is a pure in-memory diff, debounced so a burst of typing diffs once, skipped entirely when there is no baseline, and repainting only when the markers actually changed.
code_scan could already run ruff, but nothing in the UI consumed its output, so the results went into a queue nobody read. Findings are now underlined where they occur and listed in a Problems dock panel that jumps to the line on double-click. The buffer is linted rather than the file on disk, so unsaved edits are covered, and the run happens on a worker thread after typing pauses. A result from a run that has since been superseded is discarded instead of being applied to newer text. A missing or failing ruff yields no diagnostics rather than an error, so the editor keeps working without it. Parsing ruff JSON lives in utils/lint as pure logic: anything it cannot recognise is skipped, because a change in the linter output must not break the editor.
The baseline the gutter markers compare against was already in memory, so restoring a change needed no new git call: Ctrl+Alt+Z puts the block under the caret back to its committed content as a single undo step. Reverting a purely added line removes the whole line rather than blanking it, and a deletion is restored by inserting the committed lines back at that point.
GitService could only show a whole commit's diff, so seeing what changed in the file being edited meant leaving the editor. The Git menu now opens the current file's diff against its committed version in the existing side-by-side viewer, built from the baseline the editor already holds rather than a fresh git read. An unchanged file opens nothing instead of an empty tab.
Ctrl+Alt+B annotates each line with the commit, author and summary that last touched it, drawn after the line text so the code itself is not moved. The blame runs git, so it is fetched on a worker thread and only once the display is actually turned on; toggling off drops the data. Also silences the repository''s one remaining ruff finding: the Qt import in the CI smoke test must follow the platform variable it sets, which is now stated in a comment rather than left to look like an oversight.
The Encodings menu recorded the chosen encoding in the settings, but nothing on the read or write path ever consulted it: every file was opened and saved as UTF-8 regardless. Choosing an encoding now applies to the current tab. An unmodified file is re-read with it, so mojibake can be corrected in place, while a file with unsaved work only changes the encoding it will be saved with — re-reading would have thrown that work away. An encoding that cannot decode the file leaves the text alone instead of clearing it. A file''s line ending is detected on open and written back on save, so editing one line of a CRLF file no longer rewrites every line in it, and a new Line Endings menu converts between LF, CRLF and CR. Auto-save follows the same encoding and ending as the tab it belongs to.
yapf was only reachable as a manual menu action. A Format on Save toggle in the Check menu now applies the same formatting when a file is saved, for Python files only, keeping the caret on its line afterwards. The formatting itself moves into utils/format_code so the menu action and the save path share one implementation. That implementation also catches yapf''s own YapfError, which it raises instead of SyntaxError for unparseable input: without that, saving half-written code with the setting on would have failed the save, and the existing menu action would have raised.
Reopening a session brought the files back but nothing else: every file returned at line one, with its bookmarks and folds gone. Each tab now stores its caret line, bookmarked lines and folded headers alongside the file list, and restoring applies them. Restoring is best-effort: a file that shrank outside the editor simply skips the lines that no longer exist, and a hand-edited settings entry is cleaned rather than trusted, since neither should be able to break startup.
A vertical guide now marks each indentation level, and stray whitespace at the end of a line is shaded so it can be seen before it reaches a diff. Both are drawn in the viewport and can be turned off from the Style menu; guides paint behind the text so they never obscure it. Where the marks go is computed in utils/indentation as pure logic: tabs advance to the next stop rather than adding a fixed width, partial indentation rounds down to the level it reached, and a blank line shows no guide because it carries no indentation to describe.
Ctrl+Alt+\ opens a second view below the editor showing the same document: an edit in either side appears in the other at once, while scrolling and the caret stay independent, so one end of a file can be read while the other is edited. The view shares the QTextDocument rather than copying text, which also means the existing syntax highlighter — attached to the document, not the editor — colours it without a second highlighter. Closing it detaches from the shared document first, so tearing the view down cannot take the main editor''s document state with it.
Ctrl+Alt+M shows an overview of the whole file: each line is drawn as a bar following its length and indentation, so the shape of the code is recognisable, and a band marks what is currently on screen. Clicking or dragging jumps there. A file with more lines than the minimap has pixels is sampled rather than drawn line by line, and the redraw after an edit is debounced, so a large file costs the same to display as a small one.
Typing a trigger word and pressing Tab now expands a snippet, with Tab
stepping through its placeholders and selecting each default value on the
way. Once the stops run out, Tab indents as it always did, and a
selection still indents rather than expanding anything.
Snippets use the ``$1`` / ``${2:default}`` / ``$0`` notation other
editors use, so existing ones can be pasted straight into snippets.json
beside the settings. A missing or broken snippet file falls back to the
built-in Python set instead of leaving the editor unable to expand
anything, and an expansion is a single undo step.
A Tests dock panel runs pytest in the project directory and lists what it reported, failures first, with the summary line as its status. Double- clicking a row opens that test, jumping to the failing line when the test failed. The run happens on a worker thread since it spawns a subprocess, a re-entrant Run is ignored rather than dropping a thread that is still going, and closing the panel waits for it. Parsing lives in utils/test_runner as pure logic, so a result line — which also contains a colon — is never mistaken for a failure location.
Ctrl+Shift+L puts a caret at the end of every selected line and Alt-click adds or removes one anywhere; typing and Backspace then apply at all of them as a single undo step. Escape, or a plain click, goes back to one caret. QPlainTextEdit has only one caret, so the extra ones are tracked and drawn here. Edits run from the end of the document backwards, which keeps an edit from moving a position that has not been handled yet, and the carets are then advanced by how much text went in ahead of each. Backspace is refused outright when any caret sits at the start of the document, since deleting for the others would pull the carets out of step.
Completion was jedi-only, so it covered Python and nothing else. Files with another suffix now talk to a language server over stdio — TypeScript, Rust, Go, C/C++, Lua and JSON out of the box, and any other suffix through a setting — while Python keeps jedi rather than gaining a second source of completions for the same file. The client uses QProcess rather than a thread of its own, since QProcess already signals when output arrives. A server that is not installed, fails to start, or dies mid-session simply means no completions; the editor is never blocked on it, and it is shut down politely before being killed. Message framing lives in utils/lsp as pure logic: a body arriving split across reads is held until the rest turns up, and a header without a usable length or a body that is not JSON is dropped rather than raised, so a misbehaving server cannot stall the editor.
The full test suite had started crashing the interpreter partway through (0xC0000409), always at the first test that spins the event loop. Bisecting placed it at the multi-caret commit, and the cause was in its tests: a QKeyEvent built in Python and handed straight to a handler leaves Qt holding an object Python is free to collect, which is only noticed once the queued events are processed. Those tests now send keys through QTest, which keeps ownership with Qt. Two real defects surfaced while tracking that down: - Each worker was connected with a lambda, which gives Qt no receiver to disconnect, so a queued result could arrive at an editor already destroyed. The lint, git-baseline and blame managers are now QObject children of their editor and connect bound methods instead. - A manager kept holding its worker after finished/deleteLater had removed the C++ object, so the next stop() raised RuntimeError. Each now drops the reference when the worker finishes and tolerates one already gone. The git baseline and blame readers also close the GitPython Repo they open, which was leaving a pair of long-lived git subprocesses behind on every read.
The LSP client already emitted diagnostics_ready, but nothing was connected to it, so errors from a language server were fetched and then dropped. They now take the same path as ruff''s findings: the same wavy underlines, the same tooltip, the same problems panel — a non-Python file is no longer a blank slate. The two sources report different fields, so both are converted into one diagnostic shape and the display never has to know which tool found what. The ruff pass, which skips non-Python files, no longer clears the diagnostics on its way out when a server is supplying them; otherwise every pause in typing wiped what the server had just reported. CodeEditor also stops its workers in closeEvent, so a directly created editor releases its threads the way an editor tab already did.
Typing and Backspace were the only keys that reached every caret. Delete, Enter and the left/right arrows do too now, and three shortcuts grow the set: Ctrl+Alt+Shift+Up/Down puts a caret on the neighbouring line at the same column, and Ctrl+D adds one at the next occurrence of the word under the caret, wrapping to the top like other editors. Delete needed the edit arithmetic split in two: an insertion or Backspace moves the caret that made it, while Delete removes the character after the caret and so moves only what follows. A caret on a shorter line clamps to its end rather than dangling past it.
Alt-drag now puts a caret on every line the rectangle covers, at the column the pointer reached, which is what makes editing a block of lines at once possible. Dragging upwards works the same, and a line shorter than that column stops at its end rather than dangling past it. An Alt-click that does not drag still toggles a single caret, so the two gestures share the modifier without either losing out: the anchor is recorded on press and only becomes a column selection once the pointer actually moves.
The editor had no context menu at all, so cut, paste, commenting and the rest were keyboard-only. Right-click now opens Qt''s own edit menu — which already tracks what is enabled — followed by toggle comment, toggle bookmark, go to definition and revert this change. The two that depend on something being available say so by being greyed out: revert only when a git baseline is known, go to definition only when a language server is running. Definition itself is a new LSP request, with its reply parsed from any of the three shapes servers use for it.
The minimap showed the shape of the file but not where anything was. Ticks down its left edge now mark diagnostics, ticks down its right edge mark lines that differ from the last commit, and a middle column marks the other occurrences of the word under the caret — so the overview says where to look, not just how long the file is. Every marker reads state the editor has already computed, so nothing rescans the file, and moving the caret repaints on the same debounce as an edit.
Running the whole suite to check one fix was the only option the panel offered. Run Selected takes whatever is highlighted, Re-run Failures takes what failed last time — the action wanted most after a fix — and a filter box narrows the list as it is typed, keeping failures at the top. The node ids handed back to pytest are the ones it printed, passed as separate arguments rather than through a shell.
The panel listed the current tab''s findings and nothing else. It now also checks the whole project on demand, filters by severity, and applies the fixes ruff can make. Severity comes from the rule code for ruff findings and from the server''s own field for LSP ones, so both sources sort into the same three levels. Project results carry the file they came from, and double-clicking one opens that file before jumping. Applying fixes rewrites files on disk, so the open tab is re-read afterwards instead of being left on stale text.
Completion and diagnostics were all the client asked for. The context menu now also describes the symbol under the caret, renames it, and formats the document, each backed by the matching LSP request. Renaming falls back to the existing whole-file word replace when no server is running, so a Python file keeps the behaviour it had. Edits coming back from a server are applied from the end of the document backwards, so one edit never shifts another that has not been applied yet, and the batch is a single undo step. The reply parsing covers the shapes servers actually use: hover contents as a string, an object or a list of both, and edits either as a plain list or inside a WorkspaceEdit keyed by URI.
Three conveniences that were missing: - Ctrl+Shift+R records the keystrokes that follow and Ctrl+Shift+P plays them back as one undo step. Playback sends each event synchronously rather than queuing it, so the event objects live only for that call. - Typing a bracket or quote over a selection now wraps it instead of replacing it, leaving the original text selected so it can be wrapped again. - Ctrl+Alt+S saves every tab that has a file name, each with its own encoding and line ending, and running format-on-save first when that is turned on. A tab that has never been saved is skipped rather than having a location chosen for it silently.
The navigation history could only be stepped through one entry at a time. Ctrl+Alt+E now lists the lines visited recently, newest first and labelled with their text, and jumping to one takes a single choice rather than several presses of back.
Only Python was ever coloured, so every other file opened as plain text. A keyword-driven highlighter now covers TypeScript, JavaScript, Rust, Go, C, C++, Java, shell, SQL, JSON, YAML and TOML, colouring keywords, strings, comments and numbers — short of real parsing, but enough to read the shape of the code. The rules are plain data, so adding a language is a table entry rather than a class. A block comment carries its state across lines, a language without one skips that pass, and Python still uses the highlighter written for it. A file whose suffix has no rules also falls back to that one, since a new unnamed file is usually about to become Python.
Snippets could only be changed by hand-editing snippets.json, and the built-in set was Python whatever the file. A dialog now adds, edits and removes them, and saving reloads every open tab so the change is live. Snippet sets are now layered from general to specific: the built-in Python set, then the ones belonging to the file''s language (TypeScript, JavaScript, Go and Rust to start), then the user''s own — so a user definition always wins. The user file may list snippets flat or grouped by suffix, and a group meant for another language is ignored rather than leaking into this one.
Staging was all-or-nothing: GitService could only add the whole file. Stage This Change in the context menu now puts just the block under the caret into the index, by assembling the committed content with that one hunk applied and writing it as the index entry — the file on disk is left exactly as it is being edited, and every other change stays uncommitted. Reading the index back also gives a diff of staged against working, so what is about to be committed can be checked before committing it.
The debugger could be started but not directed: there was no way to say where to stop or what to do once it did. F9 toggles a breakpoint on the caret''s line, drawn in the gutter, and the marks are anchored to the text so inserting a line above one moves it with its code rather than leaving it pointing at the wrong place. F5, F10, F11 and Shift+F11 continue, step over, step into and step out by writing the matching pdb command to the running debugger, and the breakpoints can be handed to it the same way. Nothing is sent when no debugger is running, since a pdb command only means anything while it waits for input, and a broken pipe is reported rather than raised.
Seven key sequences had two owners, and Qt runs neither action when that happens, so each clash quietly disabled both features: - Ctrl+D added a caret instead of duplicating the line - Ctrl+Shift+P collided with pip install - Ctrl+Alt+S collided with Save All - Ctrl+Alt+Up/Down collided with incrementing the number under the caret - F5 and F9 collided with the toolbar Run and Debug Change-navigation moves to F7 and Shift+F7, macro playback to Ctrl+Shift+G, the caret-at-next-occurrence to Ctrl+Alt+N, breakpoints to Ctrl+F9 and Ctrl+F5, Save All to Ctrl+Shift+S, and find-and-replace to Ctrl+H so the toolbar keeps Ctrl+Shift+F for searching across files. Registration went through eight copies of the same four lines. They now share one helper backed by a registry that records who owns each sequence, logs a clash instead of letting it pass, and gives every editor command a stable name. Tests walk the real actions rather than a list, so the next duplicate fails the build.
The Problems panel ran ruff over every file directly in its refresh, so ticking "whole project" froze the window until the walk finished -- seconds on a real tree. The single-file check has always used a worker thread; this gives the project check the same treatment and reports the result through a signal, with the panel showing that a check is running meanwhile.
Breakpoints set in the gutter never reached pdb: the code that sends them existed but nothing called it, so a breakpoint was only a mark on screen and the run went straight through it. Starting the debugger now hands them over while pdb waits at the first line.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 1833 |
| Duplication | 165 |
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.
UnicodeDecodeError subclasses ValueError, so listing ValueError first made the decode handler unreachable: a non-text blob took the general branch and the reason was never logged. Both readers now try the specific one first.
The suite aborted the interpreter on Python 3.10 roughly half the time, at whichever test next spun an event loop. The message 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 the lint, baseline and blame workers but left the debounce timers running. A timer firing after the close started a fresh lint thread, and by then nothing was left to wait for it: the editor was destroyed with that thread still going. The timers now stop first, and EditorWidget closes the editor rather than stopping its managers by hand, so the two paths cannot drift apart. Each background thread also carries its name, so if this ever happens again Qt says which one. Verified on 3.10: 6 full runs with no crash, against 4 in 6 before.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What this adds
Thirty-four commits of editor work, grouped by area.
Git in the editor
F7/Shift+F7to move between them andCtrl+Alt+Zto revert the change under the caret.HEAD, staging one hunk at a time, and a staged-versus-working comparison.Ctrl+Alt+B), painted beside the line rather than in a separate pane.Baselines and blame are read on a worker thread, and the repository handle is closed after each read so
git cat-fileprocesses do not accumulate.Diagnostics
Both the single-file and project-wide checks run off the UI thread.
Editing
Multiple carets (with column selection by Alt-drag), snippets with tab stops and a per-language snippet editor, macro recording, surround-selection, recent locations, a right-click menu, indent guides and trailing-whitespace marks, a minimap marking diagnostics and changes, a split view of the same file, and syntax highlighting for eleven languages beyond Python.
Language server
Completion, hover, rename and formatting for non-Python files, over stdio with
Content-Lengthframing.Running and testing
A pytest panel that runs one test, re-runs the failures, and filters the list; breakpoints in the gutter that reach pdb when the debugger starts, with stepping bound to
F10/F11/Shift+F11.Session and files
The caret, bookmarks and folds come back with the session; the encoding menu now takes effect and line endings can be chosen; Python can be formatted on save behind a setting.
Fixes to earlier work in this branch
Ctrl+Dstopped duplicating lines, andF5/F9stopped reaching the toolbar. Registration now goes through one helper backed by a registry that records who owns each sequence and logs a clash, and the tests walk the real actions so the next duplicate fails the build.Ctrl+Dis duplicate-line again; find-and-replace moved toCtrl+Hso the toolbar keepsCtrl+Shift+Ffor searching across files.QKeyEventobjects and calling handlers directly; Qt kept the event while Python was free to collect it. They useQTest.keyClicknow. Tracking that down also turned up two real defects: signals connected to lambdas with no receiver, so a queued signal could arrive at a deleted editor, and worker wrappers left in place afterdeleteLater.YapfErrorrather thanSyntaxError, so saving half-written code failed.Verification
ruff check .clean. 1359 tests pass, run three times over to confirm the suite is stable.