Skip to content

Commit dc7492c

Browse files
committed
Add copy icon for copying text
1 parent db45827 commit dc7492c

File tree

9 files changed

+213
-14
lines changed

9 files changed

+213
-14
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
### Added
44

55
- Support for opened files without active workspaces
6+
- Icon to copy the text of the text boxes
67

78
### Changed
9+
810
- Made text expansion editor's title human-friendly
911

1012
### Fixed

src/extension/providers/JudgeViewProvider.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
import { getLogger } from "../utils/logging";
3434
import {
3535
ActionMessageSchema,
36+
CopyMessageSchema,
3637
NewInteractorSecretMessageSchema,
3738
NextMessageSchema,
3839
ProviderMessageSchema,
@@ -541,6 +542,9 @@ export default class extends BaseViewProvider<typeof ProviderMessageSchema, Webv
541542
case "VIEW":
542543
this._viewStdio(msg);
543544
break;
545+
case "COPY":
546+
void this._copyStdio(msg);
547+
break;
544548
case "STDIN":
545549
this._stdin(msg);
546550
break;
@@ -1494,6 +1498,34 @@ export default class extends BaseViewProvider<typeof ProviderMessageSchema, Webv
14941498
}
14951499
}
14961500

1501+
private async _copyStdio({ uuid, stdio }: v.InferOutput<typeof CopyMessageSchema>) {
1502+
const testcase = this._findTestcase(uuid);
1503+
if (!testcase) {
1504+
return;
1505+
}
1506+
1507+
let value: string;
1508+
switch (stdio) {
1509+
case "STDIN":
1510+
value = testcase.stdin.data;
1511+
break;
1512+
case "STDERR":
1513+
value = testcase.stderr.data;
1514+
break;
1515+
case "STDOUT":
1516+
value = testcase.stdout.data;
1517+
break;
1518+
case "ACCEPTED_STDOUT":
1519+
value = testcase.acceptedStdout.data;
1520+
break;
1521+
case "INTERACTOR_SECRET":
1522+
value = testcase.interactorSecret.data;
1523+
break;
1524+
}
1525+
1526+
await vscode.env.clipboard.writeText(value);
1527+
}
1528+
14971529
private _openInteractor() {
14981530
if (!this._currentFile) {
14991531
return;

src/extension/providers/StressViewProvider.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { getLogger } from "../utils/logging";
2626
import type JudgeViewProvider from "./JudgeViewProvider";
2727
import {
2828
AddMessageSchema,
29+
CopyMessageSchema,
2930
OpenMessageSchema,
3031
ProviderMessageSchema,
3132
SaveMessageSchema,
@@ -117,6 +118,9 @@ export default class extends BaseViewProvider<typeof ProviderMessageSchema, Webv
117118
case "VIEW":
118119
this._view(msg);
119120
break;
121+
case "COPY":
122+
void this._copy(msg);
123+
break;
120124
case "ADD":
121125
this._add(msg);
122126
break;
@@ -609,6 +613,34 @@ export default class extends BaseViewProvider<typeof ProviderMessageSchema, Webv
609613
}
610614
}
611615

616+
private async _copy({ id, stdio }: v.InferOutput<typeof CopyMessageSchema>) {
617+
const ctx = this._currentContext;
618+
if (!ctx) return;
619+
const state = ctx.state.find((s) => s.state === id);
620+
if (!state) {
621+
return;
622+
}
623+
624+
let value: string;
625+
switch (stdio) {
626+
case "STDIN":
627+
value = state.stdin.data;
628+
break;
629+
case "STDOUT":
630+
value = state.stdout.data;
631+
break;
632+
case "STDERR":
633+
value = state.stderr.data;
634+
break;
635+
case "ACCEPTED_STDOUT":
636+
case "INTERACTOR_SECRET":
637+
value = "";
638+
break;
639+
}
640+
641+
await vscode.env.clipboard.writeText(value);
642+
}
643+
612644
private _add({ id }: v.InferOutput<typeof AddMessageSchema>) {
613645
if (!this._currentFile) {
614646
return;

src/shared/judge-messages.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const ProviderMessageTypeValues = [
3030
"STDIN",
3131
"TL",
3232
"ML",
33+
"COPY",
3334
] as const;
3435

3536
export type ProviderMessageTypeValue = (typeof ProviderMessageTypeValues)[number];
@@ -64,6 +65,12 @@ export const ViewMessageSchema = v.object({
6465
stdio: StdioSchema,
6566
});
6667

68+
export const CopyMessageSchema = v.object({
69+
type: v.literal("COPY"),
70+
uuid: v.string(),
71+
stdio: StdioSchema,
72+
});
73+
6774
export const StdinMessageSchema = v.object({
6875
type: v.literal("STDIN"),
6976
uuid: v.string(),
@@ -104,6 +111,7 @@ export const ProviderMessageSchema = v.union([
104111
ActionMessageSchema,
105112
SaveMessageSchema,
106113
ViewMessageSchema,
114+
CopyMessageSchema,
107115
StdinMessageSchema,
108116
SetTimeLimitSchema,
109117
SetMemoryLimitSchema,

src/shared/stress-messages.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export const ProviderMessageTypeValues = [
7272
"RUN",
7373
"STOP",
7474
"VIEW",
75+
"COPY",
7576
"ADD",
7677
"OPEN",
7778
"CLEAR",
@@ -102,6 +103,12 @@ export const ViewMessageSchema = v.object({
102103
stdio: v.picklist(StdioValues),
103104
});
104105

106+
export const CopyMessageSchema = v.object({
107+
type: v.literal("COPY"),
108+
id: v.picklist(StateIdValue),
109+
stdio: v.picklist(StdioValues),
110+
});
111+
105112
export const AddMessageSchema = v.object({
106113
type: v.literal("ADD"),
107114
id: v.picklist(StateIdValue),
@@ -135,6 +142,7 @@ export const ProviderMessageSchema = v.union([
135142
RunMessageSchema,
136143
StopMessageSchema,
137144
ViewMessageSchema,
145+
CopyMessageSchema,
138146
AddMessageSchema,
139147
OpenMessageSchema,
140148
ResetMessageSchema,

src/webview/AutoresizeTextarea.svelte

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
onpreedit?: () => void;
1616
onsave?: () => void;
1717
oncancel?: () => void;
18+
oncopy?: () => void;
1819
variant?: Variant;
1920
actions?: Snippet;
2021
ctrlEnterNewline?: boolean;
@@ -31,6 +32,7 @@
3132
onpreedit,
3233
onsave,
3334
oncancel,
35+
oncopy,
3436
variant = "default",
3537
actions,
3638
ctrlEnterNewline = false,
@@ -39,9 +41,9 @@
3941
let textarea: HTMLTextAreaElement | undefined = $state();
4042
let containerElement: HTMLDivElement | undefined = $state();
4143
let actionButtonsElement: HTMLDivElement | undefined = $state();
42-
let isHovered = $state();
44+
let isHovered = $state(false);
4345
let cursorOverlapsActions = $state(false);
44-
let showExpandButton = $state(false);
46+
let showHoverButtons = $state(false);
4547
let showEditButtons = $state(false);
4648
4749
let canvasContext: CanvasRenderingContext2D | null = null;
@@ -158,6 +160,36 @@
158160
onexpand?.();
159161
}
160162
163+
async function handleCopy() {
164+
if (!editing && oncopy) {
165+
oncopy();
166+
return;
167+
}
168+
169+
try {
170+
await navigator.clipboard.writeText(value);
171+
} catch {
172+
if (!textarea) {
173+
const fallbackTextarea = document.createElement("textarea");
174+
fallbackTextarea.value = value;
175+
fallbackTextarea.style.position = "fixed";
176+
fallbackTextarea.style.opacity = "0";
177+
document.body.appendChild(fallbackTextarea);
178+
fallbackTextarea.focus();
179+
fallbackTextarea.select();
180+
document.execCommand("copy");
181+
document.body.removeChild(fallbackTextarea);
182+
return;
183+
}
184+
185+
const { selectionStart, selectionEnd } = textarea;
186+
textarea.select();
187+
document.execCommand("copy");
188+
textarea.selectionStart = selectionStart;
189+
textarea.selectionEnd = selectionEnd;
190+
}
191+
}
192+
161193
function handleBlur(event: FocusEvent) {
162194
const relatedTarget = event.relatedTarget as HTMLElement;
163195
if (relatedTarget?.closest(".action-buttons")) {
@@ -191,12 +223,12 @@
191223
if (containerElement) {
192224
const handleMouseEnter = () => {
193225
isHovered = true;
194-
showExpandButton = !editing && !!onexpand;
226+
showHoverButtons = !editing;
195227
};
196228
const handleMouseLeave = () => {
197229
isHovered = false;
198230
setTimeout(() => {
199-
if (!isHovered) showExpandButton = false;
231+
if (!isHovered) showHoverButtons = false;
200232
}, 200);
201233
};
202234
@@ -213,7 +245,7 @@
213245
$effect(() => {
214246
if (editing) {
215247
showEditButtons = true;
216-
showExpandButton = false;
248+
showHoverButtons = false;
217249
} else {
218250
setTimeout(() => {
219251
if (!editing) showEditButtons = false;
@@ -265,8 +297,17 @@
265297
></textarea>
266298
{/if}
267299
{#if !editing}
268-
<div class="action-buttons" class:has-buttons={showExpandButton || actions}>
269-
{#if showExpandButton && onexpand}
300+
<div class="action-buttons" class:has-buttons={showHoverButtons || actions}>
301+
{#if showHoverButtons}
302+
<button
303+
type="button"
304+
data-tooltip="Copy"
305+
aria-label="Copy"
306+
class="action-button codicon codicon-copy"
307+
onclick={handleCopy}
308+
></button>
309+
{/if}
310+
{#if showHoverButtons && onexpand}
270311
<button
271312
type="button"
272313
data-tooltip="Expand"
@@ -280,10 +321,20 @@
280321
{/if}
281322
{#if showEditButtons}
282323
<div
283-
class="action-buttons has-buttons"
324+
class="action-buttons"
325+
class:has-buttons={isHovered || !!oncancel || !!onsave}
284326
bind:this={actionButtonsElement}
285327
class:overlapped={cursorOverlapsActions}
286328
>
329+
{#if isHovered}
330+
<button
331+
type="button"
332+
data-tooltip="Copy"
333+
aria-label="Copy"
334+
class="action-button codicon codicon-copy"
335+
onclick={handleCopy}
336+
></button>
337+
{/if}
287338
{#if oncancel}
288339
<button
289340
type="button"

0 commit comments

Comments
 (0)