Skip to content

Commit dd71283

Browse files
Select and copy text in the viewer (#859)
* Select and copy text in the viewer Every pane was owner drawn with no selection anywhere in the model, so the one thing a reader wants to do with a failing snapshot - take the text somewhere else - could not be done at all. Drag across either pane to select, ctrl+c to copy, ctrl+a to select one side. Every entry's context menu also offers Copy selection, and a Copy item per pane that has anything in it. A drag arrives from a head as both of its ends at once, reported on every frame the button is held rather than as press, move and release events: the managed side already holds the selection by then, so a release has nothing to add, and a whole press-drag-release landing inside one frame still arrives whole. Rows are rows of the whole side rather than of the visible slice, because a head knows the scroll top it drew the press with and is the only thing that can resolve a drag spanning a wheel notch. A selection names the entry it was dragged in, so one left behind by an accept, a discard or a variant cycle stops existing rather than needing a clear on every transition - one of which would eventually be missed. AsciiRenderer draws a fixed width character grid and has no way to invert part of a line without changing its width, so what the model universally states about a selection goes in the status line, where every renderer draws it and the text snapshots show it, and the highlight is the enrichment the three pixel heads paint on top. The same bargain ImagePane already makes. Copying is IViewerWindow.SetClipboard rather than a ViewerActions member, because a clipboard belongs to a toolkit the way a window does: WinForms, AppKit and GLFW each own one, and the alternative was shelling out to clip, pbcopy and whichever of xclip or wl-copy happened to be installed. It is answered before the owner link, since the text is already in this process and an owner's answer would be a round trip to fetch what this one is holding. Keys.A was matched ignoring modifiers in all three heads, so ctrl+a would have accepted the snapshot rather than selecting it. DEVIEW_VERSION goes to 8: DeviewRow carries the selected run and DeviewInput the drag, with deview_set_clipboard beside them. DeviewRow is a widened array element, so this is the same kind of bump 6 was and the binaries have to be rebuilt with it. The missing note for 7 is backfilled while here. PixelTests.Selection has no Linux or macOS baseline yet; those come out of the pixel job's received artifacts once the rebuilt binaries land. * Rebuild native renderer binaries (#858) Co-authored-by: SimonCropp <122666+SimonCropp@users.noreply.github.com> * Accept the native pixel baselines The two Selection captures are new, and the Linux context menu grew the two copy items the ASCII snapshot and the WinForms one already show. All three came out of the pixel jobs' received artifacts, which is where a baseline for a renderer this machine cannot run has to come from. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: SimonCropp <122666+SimonCropp@users.noreply.github.com>
1 parent 753a9e2 commit dd71283

57 files changed

Lines changed: 1919 additions & 64 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

claude.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,19 @@ apart.
165165
three fit from `ImagePane.Width/Height` — the file header's numbers, not the decoder's — one blank
166166
line under the pane's rows, so the placement rule lives once. Headers are sniffed by hand
167167
(`ImageHeader`) rather than by System.Drawing, which does not exist on macOS or Linux.
168+
- Text selection is a view, and makes the same bargain images do. A drag arrives as both of its
169+
ends at once, in rows of the whole side rather than of the visible slice: a head knows the scroll
170+
top it drew the press with, so only it can resolve one that spans a wheel notch, and reporting
171+
the pair every held frame rather than press/move/release events is what makes a whole
172+
press-drag-release inside one frame arrive whole. `SessionState.Selection` names the entry it was
173+
dragged in, so a stale one stops existing (`LiveSelection`) rather than needing a clear on every
174+
transition, one of which would eventually be missed. `Row.Selection` is the run to highlight,
175+
which the three pixel heads draw and `AsciiRenderer` cannot - a character grid has no way to
176+
invert part of a line without changing its width - so what the model universally states about a
177+
selection goes in the **status line**, where every renderer draws it, and the highlight is the
178+
enrichment on top. Copying is `IViewerWindow.SetClipboard` rather than a `ViewerActions` member,
179+
because a clipboard belongs to a toolkit the way a window does, and it is answered before the
180+
owner link: the text is already in this process.
168181
- Queue tooltips are composed once in `QueueProjection`, not per head, and are **null when they
169182
would only repeat the row**. Labels are already the shortest distinguishing form, so the tip is
170183
what the label left off — path, test, frameworks, failure text. `QueueTooltipTests` snapshots the

docs/mdsource/viewer.source.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,20 @@ Nothing is written to disk for inline review. The patch travels over stdin, or o
7676
| `Shift+A` | Accept all |
7777
| `d` | Discard |
7878
| `v` | Cycle the variants of a conflicted snapshot (also the Variant button) |
79+
| `Ctrl+A` | Select all of one pane |
80+
| `Ctrl+C` | Copy the selection |
7981
| `q` `Esc` | Close |
8082

8183

84+
## Selecting and copying
85+
86+
Drag across either pane to select text, and `Ctrl+C` to copy it. `Ctrl+A` selects one whole pane: the one something is already selected in, or the received side when nothing is. On macOS the Edit menu carries both, so `Cmd+C` and `Cmd+A` work there too.
87+
88+
A selection belongs to one pane. Dragging out of it keeps extending within the side the drag started in rather than crossing into the other, because the two sides are different documents. It survives scrolling, so a range taller than the window is a drag plus a wheel. The status line says how much is selected, and a click with no drag behind it clears it.
89+
90+
What lands on the clipboard is what is on screen: tabs already expanded to the four spaces the panes draw them as, and no line numbers or change markers. Filler rows are left out — the blank lines that keep the two panes aligned where one side has no line are padding rather than content, so pasting a selection back gives the file's lines and nothing else.
91+
92+
8293
## Multiple pending snapshots
8394

8495
A test run that fails several inline snapshots produces one window, not several. Whichever process binds the loopback port holds the queue; everything else hands its patch to that one. The window lists everything pending and offers **Accept all**.
@@ -103,12 +114,13 @@ Every row of the pending column answers a right-click:
103114
* An inline snapshot offers **Accept**, **Discard** and **Open source file**, plus **Show next variant** when frameworks disagree about it.
104115
* A move offers **Accept move**, **Discard** and **Open target directory**; a delete offers **Accept delete**, **Discard** and **Open directory**.
105116
* A solution header offers **Accept all in ...** and **Discard all in ...** for that solution only, and a test sub-header the same for that test's changes. Bulk accepts skip conflicted snapshots, the way accept-all does.
117+
* Every entry also offers **Copy selection** when there is one, and a **Copy** item per pane, named after that pane, which copies the whole side. A side with nothing in it — the expected side of a brand new snapshot, or what is left after a delete — gets no item rather than one that copies nothing.
106118

107119
Right-clicking an entry selects it first, so the menu acts on what is highlighted. Opening a file manager is always local — the files are on this machine, wherever the queue lives.
108120

109121
On Windows and macOS this is the real OS menu, so it also takes the arrow keys, Enter, Escape and type-to-select, flips rather than clips near the edge of a screen, and is readable by a screen reader. A click that dismisses it is consumed doing so, which is why right-clicking a different row while a menu is open takes two clicks. On Linux it is drawn by the viewer, and any other click or key closes it.
110122

111-
macOS also carries a menu bar, listing the same commands as the keys below.
123+
macOS also carries a menu bar: a Snapshot menu listing the same commands as the keys above, and an Edit menu with Copy and Select All. Those two are the only items carrying a key equivalent, because the rest are plain letters and a menu would match them before the window ever saw them.
112124

113125

114126
## Grouping

docs/viewer.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,20 @@ Nothing is written to disk for inline review. The patch travels over stdin, or o
8383
| `Shift+A` | Accept all |
8484
| `d` | Discard |
8585
| `v` | Cycle the variants of a conflicted snapshot (also the Variant button) |
86+
| `Ctrl+A` | Select all of one pane |
87+
| `Ctrl+C` | Copy the selection |
8688
| `q` `Esc` | Close |
8789

8890

91+
## Selecting and copying
92+
93+
Drag across either pane to select text, and `Ctrl+C` to copy it. `Ctrl+A` selects one whole pane: the one something is already selected in, or the received side when nothing is. On macOS the Edit menu carries both, so `Cmd+C` and `Cmd+A` work there too.
94+
95+
A selection belongs to one pane. Dragging out of it keeps extending within the side the drag started in rather than crossing into the other, because the two sides are different documents. It survives scrolling, so a range taller than the window is a drag plus a wheel. The status line says how much is selected, and a click with no drag behind it clears it.
96+
97+
What lands on the clipboard is what is on screen: tabs already expanded to the four spaces the panes draw them as, and no line numbers or change markers. Filler rows are left out — the blank lines that keep the two panes aligned where one side has no line are padding rather than content, so pasting a selection back gives the file's lines and nothing else.
98+
99+
89100
## Multiple pending snapshots
90101

91102
A test run that fails several inline snapshots produces one window, not several. Whichever process binds the loopback port holds the queue; everything else hands its patch to that one. The window lists everything pending and offers **Accept all**.
@@ -110,12 +121,13 @@ Every row of the pending column answers a right-click:
110121
* An inline snapshot offers **Accept**, **Discard** and **Open source file**, plus **Show next variant** when frameworks disagree about it.
111122
* A move offers **Accept move**, **Discard** and **Open target directory**; a delete offers **Accept delete**, **Discard** and **Open directory**.
112123
* A solution header offers **Accept all in ...** and **Discard all in ...** for that solution only, and a test sub-header the same for that test's changes. Bulk accepts skip conflicted snapshots, the way accept-all does.
124+
* Every entry also offers **Copy selection** when there is one, and a **Copy** item per pane, named after that pane, which copies the whole side. A side with nothing in it — the expected side of a brand new snapshot, or what is left after a delete — gets no item rather than one that copies nothing.
113125

114126
Right-clicking an entry selects it first, so the menu acts on what is highlighted. Opening a file manager is always local — the files are on this machine, wherever the queue lives.
115127

116128
On Windows and macOS this is the real OS menu, so it also takes the arrow keys, Enter, Escape and type-to-select, flips rather than clips near the edge of a screen, and is readable by a screen reader. A click that dismisses it is consumed doing so, which is why right-clicking a different row while a menu is open takes two clicks. On Linux it is drawn by the viewer, and any other click or key closes it.
117129

118-
macOS also carries a menu bar, listing the same commands as the keys below.
130+
macOS also carries a menu bar: a Snapshot menu listing the same commands as the keys above, and an Edit menu with Copy and Select All. Those two are the only items carrying a key equivalent, because the rest are plain letters and a menu would match them before the window ever saw them.
119131

120132

121133
## Grouping

native/include/deview.h

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ typedef struct DeviewRow {
5151
int32_t lineNumber;
5252
int32_t textOffset;
5353
int32_t textLength;
54+
55+
/*
56+
* What of this row the reader has selected, in characters of the text above rather than in
57+
* pixels: the managed side flattens tabs before it counts, so a column here multiplied by the
58+
* cell width is where the highlight goes.
59+
*
60+
* selectLength is 0 on a row with nothing selected, which is every row of almost every frame.
61+
* The managed side has already resolved which side the drag is in and clipped the range to the
62+
* visible slice, so a renderer only fills a rectangle and never decides what is in it.
63+
*/
64+
int32_t selectStart;
65+
int32_t selectLength;
5466
} DeviewRow;
5567

5668
typedef struct DeviewPane {
@@ -168,7 +180,11 @@ enum DeviewKey {
168180
DEVIEW_KEY_DISCARD = 12,
169181
DEVIEW_KEY_ACCEPT_ALL = 13,
170182
DEVIEW_KEY_QUIT = 14,
171-
DEVIEW_KEY_NEXT_VARIANT = 15
183+
DEVIEW_KEY_NEXT_VARIANT = 15,
184+
/* Ctrl+C, and Cmd+C on macOS. */
185+
DEVIEW_KEY_COPY = 16,
186+
/* Ctrl+A, which is why plain A must be reported as accept only when no modifier is held. */
187+
DEVIEW_KEY_SELECT_ALL = 17
172188
};
173189

174190
typedef struct DeviewInput {
@@ -203,6 +219,26 @@ typedef struct DeviewInput {
203219
*/
204220
int32_t columns;
205221
int32_t rows;
222+
223+
/*
224+
* A drag selecting pane text: which pane (0 left, 1 right, -1 for the frames with no button
225+
* held over one), and both of its ends.
226+
*
227+
* Reported together for as long as the button is held and then simply not reported, rather
228+
* than as press, move and release events. The managed side already holds the selection by
229+
* then, so a release has nothing to add, and a whole press-drag-release landing inside one
230+
* frame still arrives whole.
231+
*
232+
* Rows are rows of the whole side, not of the visible slice: this side drew the press with a
233+
* scrollTop it still remembers, so only this side can resolve a drag that spans a wheel notch.
234+
* Columns are characters of the row's text. Neither is clamped here — the managed side owns
235+
* the text and does it there.
236+
*/
237+
int32_t dragSide;
238+
int32_t dragAnchorRow;
239+
int32_t dragAnchorColumn;
240+
int32_t dragFocusRow;
241+
int32_t dragFocusColumn;
206242
} DeviewInput;
207243

208244
/*
@@ -221,8 +257,15 @@ typedef struct DeviewInput {
221257
* 6: DeviewPane carries the picture the side is, so an image comparison is drawn rather than only
222258
* described. A widened array element, so an older library reads every pane after the first at
223259
* the wrong offset — this is the bump that matters most to honour.
260+
* 7: DeviewScreen carries the pending count, which is not the length of the queue it also carries:
261+
* that is the visible slice, so a head deriving one from the other said "Pending (16)" beside
262+
* "inline 1 of 30".
263+
* 8: DeviewRow carries the selected run of its text and DeviewInput reports a drag across a pane,
264+
* which between them are text selection. DeviewRow is a widened array element, so this is the
265+
* same kind of bump 6 was. deview_set_clipboard is added beside them, because the selection is
266+
* only worth having if it can be copied and each toolkit owns its own clipboard.
224267
*/
225-
#define DEVIEW_VERSION 7
268+
#define DEVIEW_VERSION 8
226269

227270
/*
228271
* The Swift implementation imports this header for the struct layouts, because Swift does not
@@ -266,6 +309,12 @@ DEVIEW_API int32_t deview_capture(
266309

267310
DEVIEW_API void deview_set_hidden(int32_t hidden);
268311

312+
/*
313+
* Puts UTF-8 text on the system clipboard. The toolkit's own clipboard rather than the managed
314+
* side shelling out to a copy program, which is a thing every desktop has a different one of.
315+
*/
316+
DEVIEW_API void deview_set_clipboard(const char* text);
317+
269318
DEVIEW_API void deview_focus(void);
270319

271320
DEVIEW_API void deview_shutdown(void);

0 commit comments

Comments
 (0)