Skip to content

Commit 57681f7

Browse files
Isolator acmclaude
authored andcommitted
feat(results): ⌘A in a raw result pane selects just that text
In TSV/JSON output mode the result is a plain <div>, so ⌘A fell through to the browser's whole-page select-all. Make the raw (.raw-text-view) and JSON (.json-view) panes focusable (tabindex=0) and, when ⌘/Ctrl+A fires with focus inside one, select that node's contents via Selection.selectAllChildren so it can be copied. Focus elsewhere (e.g. the editor textarea) still falls through to the native select-all, so ⌘A in the SQL area keeps selecting the whole query. Verified in-browser: ⌘A in a focused TSV pane selects exactly the TSV text; ⌘A in the editor is not intercepted. 325 tests pass; shortcuts/results at 100%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QennTvGKAtJZrv9EpQagef
1 parent 8680ec3 commit 57681f7

4 files changed

Lines changed: 38 additions & 2 deletions

File tree

src/styles.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ body {
348348
color: var(--fg); white-space: pre;
349349
background: var(--bg);
350350
}
351+
/* Focusable (tabindex) so a click scopes ⌘A to the pane's text; no focus ring. */
352+
.raw-text-view:focus, .json-view:focus { outline: none; }
351353

352354
/* ------------ chart placeholder ------------ */
353355
.chart-view {

src/ui/results.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export function renderResults(app) {
3333
} else if (r.error) {
3434
inner.appendChild(h('div', { class: 'results-error' }, r.error));
3535
} else if (r.rawText != null) {
36-
inner.appendChild(h('div', { class: 'raw-text-view' }, r.rawText));
36+
inner.appendChild(h('div', { class: 'raw-text-view', tabindex: '0' }, r.rawText));
3737
} else if (r.rows.length === 0) {
3838
inner.appendChild(h('div', { class: 'placeholder' }, h('div', null, 'Query returned 0 rows.')));
3939
} else if (app.state.resultView === 'json') {
@@ -87,7 +87,7 @@ export function renderJson(r) {
8787
r.columns.forEach((c, i) => { o[c.name] = row[i]; });
8888
return o;
8989
});
90-
return h('div', { class: 'json-view' }, JSON.stringify(arr, null, 2));
90+
return h('div', { class: 'json-view', tabindex: '0' }, JSON.stringify(arr, null, 2));
9191
}
9292

9393
export function renderTable(app, r) {

src/ui/shortcuts.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,17 @@ export function handleKeydown(e, app) {
7373
app.actions.toggleSaved();
7474
return 'toggleSaved';
7575
}
76+
if (mod && e.key.toLowerCase() === 'a') {
77+
// Inside a raw result pane (TSV / JSON output), select just that text so it
78+
// can be copied — not the whole page. Elsewhere (e.g. the editor textarea)
79+
// fall through to the browser's native select-all.
80+
const t = e.target;
81+
const box = t && t.closest && t.closest('.raw-text-view, .json-view');
82+
if (!box) return null;
83+
e.preventDefault();
84+
box.ownerDocument.defaultView.getSelection().selectAllChildren(box);
85+
return 'selectAll';
86+
}
7687
if (e.key === '?' && !mod) {
7788
const t = e.target;
7889
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return null;

tests/unit/shortcuts.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,27 @@ describe('handleKeydown', () => {
8989
expect(handleKeydown(ev({ key: 'x' }), app)).toBeNull();
9090
expect(handleKeydown(ev({ key: '?', target: null }), makeApp())).toBe('shortcuts');
9191
});
92+
93+
it('⌘A inside a raw result pane selects just that text', () => {
94+
const app = makeApp();
95+
const box = document.createElement('div');
96+
box.className = 'raw-text-view';
97+
box.textContent = 'a\tb\nc\td';
98+
document.body.appendChild(box);
99+
const e = ev({ metaKey: true, key: 'a', target: box });
100+
expect(handleKeydown(e, app)).toBe('selectAll');
101+
expect(e.preventDefault).toHaveBeenCalled();
102+
expect(box.ownerDocument.defaultView.getSelection().toString()).toBe('a\tb\nc\td');
103+
});
104+
105+
it('⌘A elsewhere falls through to the native select-all', () => {
106+
const app = makeApp();
107+
// editor textarea (no raw-pane ancestor) → not handled
108+
const ta = document.createElement('textarea');
109+
const e = ev({ metaKey: true, key: 'A', target: ta });
110+
expect(handleKeydown(e, app)).toBeNull();
111+
expect(e.preventDefault).not.toHaveBeenCalled();
112+
// no target at all
113+
expect(handleKeydown(ev({ metaKey: true, key: 'a', target: null }), app)).toBeNull();
114+
});
92115
});

0 commit comments

Comments
 (0)