Skip to content

Commit 2c96752

Browse files
committed
Merge branch 'mouse-and-clipboard' into copy-paste-images
2 parents f9193c5 + 77e7819 commit 2c96752

7 files changed

Lines changed: 49 additions & 34 deletions

File tree

lib/src/components/SelectionOverlay.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ function computeRects(
8080
for (let r = firstRow; r <= lastRow; r++) {
8181
let c0 = 0;
8282
let c1 = cols;
83-
if (r === n.r0) c0 = n.c0;
83+
if (r === n.r0) c0 = n.c0;
8484
if (r === n.r1) c1 = n.c1 + 1;
85-
if (c1 <= c0) continue;
85+
if (c1 <= c0) continue;
8686
rects.push({
8787
top: (r - viewportStart) * cellHeight,
8888
left: c0 * cellWidth,

lib/src/lib/mouse-mode-observer.test.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,8 @@ describe('attachMouseModeObserver', () => {
9393
});
9494

9595
it('dispose tears down both handlers', () => {
96-
const { terminal } = buildMockTerminal();
97-
const mockDispose1 = vi.fn();
98-
const mockDispose2 = vi.fn();
99-
const realParser = terminal.parser;
100-
(terminal as unknown as { parser: unknown }).parser = {
101-
registerCsiHandler(_id: unknown, _cb: unknown) {
102-
return realParser === terminal.parser
103-
? { dispose: mockDispose1 }
104-
: { dispose: mockDispose2 };
105-
},
106-
};
107-
108-
// Simpler: build a fresh mock with explicit disposables
10996
const disposables: Array<{ dispose: ReturnType<typeof vi.fn> }> = [];
110-
const term2 = {
97+
const terminal = {
11198
parser: {
11299
registerCsiHandler() {
113100
const d = { dispose: vi.fn() };
@@ -118,7 +105,7 @@ describe('attachMouseModeObserver', () => {
118105
modes: { mouseTrackingMode: 'none', bracketedPasteMode: false },
119106
} as unknown as Terminal;
120107

121-
const observer = attachMouseModeObserver('a', term2);
108+
const observer = attachMouseModeObserver('a', terminal);
122109
observer.dispose();
123110

124111
expect(disposables).toHaveLength(2);

lib/src/lib/mouse-selection.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
__resetMouseSelectionForTests,
55
beginDrag,
66
endDrag,
7+
flashCopy,
78
getMouseSelectionSnapshot,
89
getMouseSelectionState,
910
isDragging,
@@ -272,6 +273,23 @@ describe('mouse-selection: drag lifecycle', () => {
272273
});
273274
});
274275

276+
describe('mouse-selection: flashCopy race', () => {
277+
it('beginDrag during a flash clears copyFlash so the timer does not nuke the new selection', () => {
278+
beginDrag('a', { row: 0, col: 0, altKey: false, startedInScrollback: false });
279+
updateDrag('a', { row: 3, col: 5, altKey: false });
280+
endDrag('a');
281+
282+
// Simulate flashCopy — but we call beginDrag before the timer fires.
283+
flashCopy('a', 'raw', 500);
284+
expect(getMouseSelectionState('a').copyFlash).toBe('raw');
285+
286+
// New drag starts before the 500ms timer.
287+
beginDrag('a', { row: 10, col: 2, altKey: false, startedInScrollback: false });
288+
expect(getMouseSelectionState('a').copyFlash).toBeNull();
289+
expect(getMouseSelectionState('a').selection?.startRow).toBe(10);
290+
});
291+
});
292+
275293
describe('mouse-selection: snapshot caching', () => {
276294
it('returns the same snapshot reference between changes', () => {
277295
setMouseReporting('a', 'vt200');

lib/src/lib/mouse-selection.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ export function beginDrag(
149149
args: { row: number; col: number; altKey: boolean; startedInScrollback: boolean },
150150
): void {
151151
const s = ensure(id);
152+
// Clear any in-flight copy flash so its timer won't null out this new
153+
// selection when it fires (the timer checks `copyFlash !== kind`).
154+
s.copyFlash = null;
152155
s.selection = {
153156
startRow: args.row,
154157
startCol: args.col,

lib/src/lib/selection-text.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ export function extractSelectionText(terminal: Terminal, sel: Selection): string
5252
for (let r = n.r0; r <= n.r1; r++) {
5353
const line = buf.getLine(r);
5454
if (!line) continue;
55-
const c0 = r === n.r0 ? n.c0 : 0;
55+
const c0 = r === n.r0 ? n.c0 : 0;
5656
const c1 = r === n.r1 ? n.c1 + 1 : terminal.cols;
57-
lines.push(line.translateToString(false, c0, c1).replace(/\s+$/, ''));
57+
lines.push(line.translateToString(false, c0, c1).replace(/\s+$/, ''));
5858
}
5959
return lines.join('\n');
6060
}

lib/src/lib/smart-token.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ describe('detectTokenAt: path', () => {
8787
expect(t?.text).toBe('src/foo.ts:42:7');
8888
});
8989

90+
it('error location with trailing period is detected after stripping', () => {
91+
const t = at('Error at src/foo.ts:42. See docs.', 'src/');
92+
expect(t).toMatchObject({ kind: 'path', text: 'src/foo.ts:42' });
93+
});
94+
9095
it('strips trailing period on absolute path', () => {
9196
const t = detectTokenAt('/tmp/a.', 0);
9297
expect(t?.text).toBe('/tmp/a');

lib/src/lib/smart-token.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,16 @@ export interface DetectedToken {
1616
interface Pattern {
1717
kind: 'url' | 'path';
1818
re: RegExp;
19-
/** When true, trailing-punctuation stripping is skipped — the pattern's
20-
* trailing characters are significant (e.g. error-location `:line:col`). */
21-
skipStrip?: boolean;
2219
}
2320

2421
const PATTERNS: Pattern[] = [
25-
{ kind: 'url', re: /^https?:\/\/\S+$/ },
26-
{ kind: 'url', re: /^file:\/\/\S+$/ },
27-
{ kind: 'path', re: /^\S+:\d+(:\d+)?$/, skipStrip: true }, // error-location first (so it beats generic path)
28-
{ kind: 'path', re: /^~\/\S*$/ },
29-
{ kind: 'path', re: /^\/\S+$/ },
30-
{ kind: 'path', re: /^\.\.?\/\S*$/ },
31-
{ kind: 'path', re: /^[A-Za-z]:\\\S*$/ },
22+
{ kind: 'url', re: new RegExp('^https?://\\S+$') },
23+
{ kind: 'url', re: new RegExp('^file://\\S+$') },
24+
{ kind: 'path', re: new RegExp('^\\S+:\\d+(:\\d+)?$') }, // error-location first (so it beats generic path)
25+
{ kind: 'path', re: new RegExp('^~/\\S*$') },
26+
{ kind: 'path', re: new RegExp('^/\\S+$') },
27+
{ kind: 'path', re: new RegExp('^\\.{1,2}/\\S*$') },
28+
{ kind: 'path', re: new RegExp('^[A-Za-z]:\\\\\\S*$') },
3229
];
3330

3431
const TRAILING_PUNCT = /[.,;:!?'"]+$/;
@@ -88,11 +85,16 @@ export function detectTokenAt(line: string, col: number): DetectedToken | null {
8885
const raw = line.slice(start, end);
8986
if (!raw) return null;
9087

91-
for (const { kind, re, skipStrip } of PATTERNS) {
92-
if (!re.test(raw)) continue;
93-
const text = skipStrip ? raw : stripTrailing(raw);
94-
if (!text) continue;
95-
return { kind, start, end: start + text.length, text };
88+
// Strip trailing punctuation once, then test all patterns against the
89+
// cleaned token. This ensures error-location patterns like `file:42` are
90+
// found even when the original token had a trailing period (e.g. in
91+
// compiler output "Error at src/foo.ts:42.").
92+
const cleaned = stripTrailing(raw);
93+
if (!cleaned) return null;
94+
95+
for (const { kind, re } of PATTERNS) {
96+
if (!re.test(cleaned)) continue;
97+
return { kind, start, end: start + cleaned.length, text: cleaned };
9698
}
9799
return null;
98100
}

0 commit comments

Comments
 (0)