14: Compare and Differences - transcript
Jessica: The QUILL Cast, episode fourteen. I'm Jessica. Today, the feature that pays for itself the first time you say wait, what changed: compare and differences. The machinery for asking two documents what they disagree about, and for hearing the answer in a way that respects your ears, your keyboard, and your time.
Liam: I'm Liam. This is the tool you reach for when two versions of a document exist and your brain refuses to eyeball the diff. A redline you can hear. A side-by-side you can step through, one group at a time, with the screen reader narrating exactly what moved.
Jessica: Quick recap of episode thirteen, where we lived inside the word tools. Spell check, real dictionary, real thesaurus, and the four navigation verbs for misspelling walking. We also promised a follow-up on review-grade diffing, and that is exactly what this episode delivers. Spelling catches typos. Compare catches the moments a reviewer, a co-author, a translation pass, or a long idle afternoon quietly rearranged your paragraph.
Liam: Before we get into keystrokes, here is the honest part. We have two compare systems in the codebase. Both are real, both ship, both do useful work, and they are not the same tool. The naming around them has drifted. We are going to walk through what each one does, where each one lives in the menu, and how to pick the right one for the job. We are also going to call out where the documentation in earlier episodes, and even our own episode outline, is wrong about the canonical F8 behaviour, and why.
Jessica: A small heads-up about the source files, because Jeff and our reviewers asked us to be precise. The brief for this episode pointed at main_frame_compare.py and compare.py. Neither of those files exists. The actual implementation is in two places. quill.core.compare_service is the new, pure-Python diff engine, no wx imports, runs the algorithm and owns the navigation state. quill.ui.compare_dialog is the modal keyboard-first dialog that wraps it. There is also a separate, older compare system, _CompareSession, that lives directly inside main_frame.py around lines six-thirteen through twenty-one-two-eighty. The two systems coexist right now, and the menus expose both.
Liam: That sounds like a mess. It is not quite a mess. They cover different shapes of problem, and the audio studio work earlier this year made sure both are reachable from the keyboard. We are going to be honest about that coexistence because pretending it does not exist would be worse than naming it. If you are a power user who edits your keymap, you may notice some commands in the dialog layer and some in the session layer. Both are correct. Both work. Let us show you what each one does.
Jessica: Let us start with the newer one, the keyboard-first compare mode. The menu path is Navigate, Compare, Compare with File. The command id is tools.compare_with_file, the registered accelerator in our default keymap is currently empty, and the practical way to launch it is the menu, or the QUILL-key chord, ctrl-shift-grave, then C for compare. The dialog appears, sized eight-twenty by five-sixty, and QUILL reads the status line aloud. Comparing left and right. N difference or differences found. Press F8 for next, shift F8 for previous, escape to close.
Liam: Inside that dialog, F8 advances to the next difference, shift F8 walks backward, control F8 re-speaks the current difference, alt F8 reads the inline change, control shift F8 toggles whitespace ignoring, escape closes. Those are the documented bindings inside the modal. They are not the same F8 you would press in the editor with no compare session running. We will come back to that distinction in a moment, because it is the single most important call-out in this episode.
Jessica: Do this now, before we go any further. Open two short files you have lying around, maybe a draft and a revision, or two chapters of the same manuscript. In QUILL, click into the first file. Open the Navigate menu, then Compare, then Compare with File. Pick the second file. QUILL computes a structured diff and opens the dialog. The status line tells you how many groups were found. The Differences list on the left is a stock wx listbox, fully accessible. The detail area on the right is a read-only rich text control, also fully accessible. Use F8 to walk forward, shift F8 to walk back, and just listen. Do not read the screen yet. Just listen. Pause the audio if you need more time with the dialog. We will be here when you come back.
Liam: Welcome back. If you only remember one thing from this segment, it is the difference classification. The service identifies five kinds of change. Insert, lines added. Delete, lines removed. Replace, lines that swapped for different lines. Whitespace, lines that look identical when spaces and tabs are normalised. Line ending, a change that is purely CRLF versus LF, the bane of cross-platform editors. Each kind gets a different spoken summary, and a different format in the detail area. Knowing the kind matters because the fix is different. A whitespace change usually means turn off a sloppy editor. A line-ending change usually means normalize. An insert or delete is content. A replace is content that you actually have to read.
Jessica: Behind that classification is a layered diff. The first pass uses difflib SequenceMatcher on whole lines. The second pass, only when the change is a single line replaced by another single line, runs another SequenceMatcher at the character level, and that produces inline spans. The detail area uses those spans to tell you exactly which characters moved. If the only change between two versions of a sentence is the word color becoming colour, the inline summary will say exactly that. Word-level change. Changed color to colour. That is the change, no padding, no surrounding line noise.
Liam: The toggle for whitespace is the most-misunderstood control on the dialog, so we will spend a minute on it. Three states. Ignore trailing whitespace only. Ignore all whitespace, which collapses runs of spaces and tabs to nothing. And exact, which is the default and the strictest. The toggle cycles through these on each press. Why would you ever want ignore all? Because if you and a colleague both work in editors that auto-indent differently, every blank run of spaces becomes a phantom difference. Toggle on, and the diff becomes meaningful. Toggle off, and you can see the cosmetic noise. Both views are honest, and you should be able to flip between them without restarting the comparison.
Jessica: The dialog also has a list of differences on the left, and that list is not just decorative. You can arrow into it, press enter on any group, and the detail area jumps to that group. For a long comparison, dozens of groups, this is faster than hammering F8. The list selection stays in sync with the F8 cursor, so the two views are one navigation state, two presentations. That is a small detail that pays off when a reviewer asks you to skip to group seven, and your hands are already on the keyboard.
Liam: One more thing about the modal dialog before we move on. When it closes, the CompareService state is gone. The dialog does not save your last comparison. If you want to keep the differences, generate a report before closing, and we will show you that in a moment. If you just want to inspect and move on, escape, and it is done. The session does not leak back into the editor.
Jessica: Now the older compare system, the one that lives in main_frame.py and shows up in the Tools menu as Comparison. The menu path is Tools, Comparison, Compare with File. Or Tools, Comparison, Compare Open Documents. The accelerator binding for the navigation commands in this system, again, per the current keymap, is Ctrl+Alt+Shift-period for next difference, Ctrl+Alt+Shift-comma for previous, and Ctrl+Alt+Shift-D for announce. That is the canonical binding after the keymap consolidation work in issue three-fifty-seven.
Liam: You may be wondering why we have two systems. Here is the honest answer. The newer CompareService and CompareDialog is the keyboard-first, structured, screen-reader-native path. The session-based _CompareSession system is the multi-document path. It can compare more than two documents at once. It can focus a specific document at a difference. It produces a summary document. It opens the diff as a unified-diff text view in a message box. The two are not duplicates, they are siblings with different strengths.
Jessica: Let us walk the session path. With two or more documents open, go to Tools, Comparison, Compare Open Documents. QUILL iterates over the open tabs, line by line, finds the line ranges where the documents disagree, and stores them as a list of _CompareDifferenceGroup objects. Each group has a classification, similar in spirit to the new system, but with its own taxonomy. Added, lines present in the second document but not the first. Removed, lines present in the first but not the second. Changed, lines that are textually different. Case-only, the text matches ignoring case. Whitespace-only. Punctuation-only. Unicode-only, when the text matches under NFKC normalisation, which folds things like the fullwidth comma into a regular comma. And finally, a generic changed bucket when none of the smarter categories apply.
Liam: That classification is auditable, and it is the kind of thing a compliance reviewer loves. The system does not say text changed. It says this changed only in punctuation, or only in case, or only under Unicode normalisation. If you are reviewing a legal document where the only difference between two drafts is the placement of Oxford commas, the classification tells you that in one word, and you can sign off with confidence.
Jessica: Once a comparison session is active, the navigation verbs are still F8, shift F8, and control F8, but now they are intercepted at the editor level, in main_frame.py around line forty-eight-hundred-five. The condition is strict. The F8 handler fires only when self._compare_session is not None. If no session is running, F8 falls through to the normal selection behavior, edit.start_selection, which is the default binding in the keymap. This is the source of the most common confusion we hear about compare.
Liam: So let us say it slowly. F8 does not always mean compare-next. F8 means compare-next only inside an active compare session, and inside the modal CompareDialog. Outside those two states, F8 means start-selection, the EdSharp-style mark-and-extend binding we have used since the early port. If you press F8 in the editor with no comparison running, you do not get a compare error, you get a selection mark. The two commands share the same key, in different contexts, and the context is the only thing telling them apart.
Jessica: This is a real piece of doc drift. The brief for this episode listed F8 as the canonical keybinding for compare. The truth is, the canonical binding, the one in the keymap, is Ctrl+Alt+Shift-period, alongside the AI reserved chord class. F8 is the in-context shortcut, and it works because the editor and the dialog both intercept it. If you are documenting compare for someone, document both bindings. If you are using compare, use the in-context F8 inside the dialog or session, and the chord outside. Both are correct. The code is doing what it should.
Liam: And to be even more honest, the keymap itself is also slightly out of date in a couple of ways. The compare commands registered in main_frame_menu.py still use the old id names, _id_compare_with_file and friends, and they sit under both Navigate, Compare, and Tools, Comparison. The keyboard binding registry maps the tools.compare_next_difference id to Ctrl+Alt+Shift-period, but the F8 in-context handler does not go through that registry. It is a hard-coded if-branch in the editor's _on_editor_key_down. If you rebind compare-next through the keymap editor, you change the chord binding, not the in-dialog F8 behavior. That is by design, but it is worth saying out loud.
Jessica: The session-based system has three more moves worth knowing. The first is synchronized navigation. By default, when you advance to a difference, QUILL moves the caret in the active document to the relevant line, so your editor cursor and the comparison cursor stay aligned. Toggle that off with the menu item, or with control alt F8, and QUILL will narrate the difference without disturbing your cursor. Useful when you are walking differences in a long manuscript and you do not want the screen to scroll away from where you were reading.
Liam: The second is the Difference List dialog. With a session running, open the Tools, Comparison, Difference List menu, or hit alt F8 in the editor. A SingleChoiceDialog appears, with one line per group, each starting with the kind, the line number, and a sixty-character preview. Pick one, hit enter, and QUILL jumps the comparison cursor to that group. The preview is the first line of the first document in the group, which is usually enough to spot the spot you wanted.
Jessica: The third is the summary. Tools, Comparison, Create Difference Summary, and QUILL opens a new tab with a human-readable summary of every group. The summary lists the documents, the current options, and each group in order. This is the artifact you send to a colleague who is not at your desk, the artifact you attach to a ticket, the artifact you save into the project folder. Generating it is one menu pick. The data is already there.
Liam: And, of course, Copy Current Difference and Copy All Differences. The first copies just the current group, the kind plus the line ranges plus the line previews, formatted for chat or email. The second copies the entire summary. Both land on the system clipboard, ready to paste anywhere. The Copy Current verb is the right tool when a reviewer asks a single targeted question. Copy All is the right tool when the reviewer is about to walk away and you want them to have the whole thing.
Jessica: Let us talk about the kind classification one more time, because the new system and the old system use different words, and the episode outline was already getting confused. In the modal CompareDialog system, the kinds are insert, delete, replace, whitespace, and line-ending. In the session-based _CompareSession system, the kinds are added, removed, changed, case-only, whitespace-only, punctuation-only, unicode-only, and changed as a catch-all. Neither is wrong, neither is a typo. They are two different diff engines optimized for two different audiences. The modal one speaks to a screen-reader user stepping through two files. The session one speaks to a reviewer auditing many documents. Pick the engine that matches your task, and the vocabulary will make sense.
Liam: For a typical review pass, the new modal CompareDialog is the right starting point. Open it from Navigate, Compare, Compare with File, walk the differences with F8, listen to each one, fix what needs fixing in the source document, then re-run. For an audit pass, when you have several open documents and need a single artifact to share, the session-based Tools, Comparison system is the right tool. Compare Open Documents, walk with F8, build a summary, copy or save.
Jessica: And here is the workflow recipe to steal. Three steps, repeatable. Step one, identify which engine matches the task. Two files versus many, structured diff versus unified text, in-dialog F8 versus in-editor F8. Step two, run the compare. Step three, choose your output. Listen in the dialog, save a report from the dialog, generate a summary document from the session, or copy a single group to the clipboard. You can mix and match. The two systems do not lock you in to one verb.
Liam: A piece of internal information for the curious. The compare service is structured so that all diffing happens in pure Python, off the UI thread, in quill.core.compare_service. The wx layer, quill.ui.compare_dialog, owns the widgets and the keyboard handling. That separation is deliberate. It means the diff engine can be unit-tested without wx, the dialog can be exercised without a real diff, and a future web or scripting front-end can call the same service. If you ever want to compare two strings from a Quillin or a script, import CompareService from quill.core.compare_service and call .compare with your two texts. The output is a list of DifferenceGroup objects, fully typed, fully self-describing.
Jessica: And the session system, for all its quirks, is also cleanly separated. _CompareSession is a dataclass, _CompareDifferenceGroup is a dataclass, _CompareLineBlock is a dataclass. The data is plain Python. The classification logic, _classify_difference_kind, is a single function you can read end to end. The unified-diff helper, quill.core.diffing.build_unified_diff, is a one-screen wrapper around Python's built-in difflib. None of this is hidden. All of it is on a path that respects the layered architecture the project insists on.
Liam: Let us also name one limitation, because honest tools acknowledge their limits. The session-based compare system uses a line-based diff. If two documents differ only in the middle of one long line, the entire line shows up as changed, and the inline text is presented as a single block, not as a character-level highlight. The modal CompareDialog does the inline character-level diff, but only for single-line replacements, not for multi-line ones. There is no full character-level paint across multi-line hunks in either system today. If you need that, the third-party tool is a unified diff in a code editor, and the export pathway from QUILL is the summary document plus the unified-diff text dump that Tools, Comparison, Compare with File shows you in a message box.
Jessica: Quick mention of the Compare with Remote menu item, the one in the publishing compare submenu. It is a publishing workflow, not a file-diff workflow. You point QUILL at a remote item, and QUILL fetches a remote version of that item and compares it against the local one. Different code path, different use case. We will visit publishing-compare in its own episode later in the series. For today, file and document compare is what we are covering.
Liam: A note on accessibility, since this episode is on the audible-tools track of the show. Both compare systems are screen-reader-native. The modal CompareDialog has an accessible listbox, an accessible rich text detail area, an accessible checkbox for the whitespace toggle, and accessible buttons for previous, next, read current, and close. The session-based system uses a SingleChoiceDialog for the difference list, which is a stock wx control, and the navigation verbs all hit the same _announce path that every other compare in the app uses. The keyboard hooks at the editor level are explicit. There is no compare mode in QUILL that requires a mouse.
Jessica: And on focus, the modal dialog hands focus to the differences list when it opens, so the screen reader announces the first summary immediately. The list, the detail, the checkbox, and the buttons are all reachable by tab, in a sensible order, and the dialog contract through apply_modal_ids ensures the escape and cancel behavior is consistent across the app. The dialog inventory gate, the one we mentioned back in episode four, audits every modal dialog for that contract. Compare is in the inventory. Compare is compliant.
Liam: Let us talk about one last edge case. The Compare with File dialog in the modal CompareDialog system reads the right-hand file with utf-8 and errors-replace, which means an unreadable byte becomes a question mark, not a crash. The session-based Compare with File uses quill.io to read the right-hand file, which honours the same encoding selection as opening a file through File, Open. Two different readers, two different encoding paths, both honest about what they did. If you ever compare two files and the diff shows stray question marks, the encoding is your culprit, not the diff engine.
Jessica: For the document rescue and OCR tracks, which we will cover in later episodes, the same CompareDialog is a useful sanity check. After OCR, you have a recognized text and a clean original. Compare with File, walk the differences, and you can hear exactly which characters the OCR got wrong. The word-level summary in the dialog makes this fast, because most OCR errors are single-character substitutions, and the inline spans highlight exactly the swapped characters.
Liam: Which brings us to homework. Four steps. Step one, run the modal CompareDialog on two files you have lying around, and walk the diff with F8. Listen to every group, do not read the screen. Step two, toggle the whitespace handling and re-run, and notice how the count of groups changes. Step three, with two or more documents open, run Tools, Comparison, Compare Open Documents, open the Difference List, jump to group three or four, and generate a summary document. Step four, if you have a workflow that compares the same two files regularly, write yourself a one-line Quillin that imports CompareService and prints the group count. That is your future self thanking present self.
Jessica: Next episode, episode fifteen, we move from reviewing the past to deploying the future. Clipboard, snippets, and macros. The text-supply toolkit, the copy tray, the abbreviation manager, the action builder, the whole machinery of saying a thing once and having QUILL produce it on demand. Think of compare as the tool for asking what changed. Snippets are the tool for making the change once and reusing it forever. Same keyboard-first energy, same screen-reader-native contract, different job.
Liam: That is the arc. Review the past, deploy the present, build the future. Three episodes, three hats.
Jessica: The QUILL Cast is fifty-four episodes total, and we are in the everyday-editor stretch. You have made it through spell, find, format, save, and now compare. The next block opens the supply closet. Bring a coffee, and bring a sentence you have written three different ways, because the next episode is going to ask you to write it a fourth time, faster.
Liam: I'm Liam.
Jessica: I'm Jessica. Compare carefully.