Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ jobs:
bun test src/features/editor/stores/editor-app.store.test.ts src/features/editor/services/document-external-change-workflow.test.ts src/features/editor/services/document-save-lifecycle.test.ts
bun test src/features/editor/lsp/language-server-navigation.test.ts src/features/editor/lsp/java-navigation-marker-loader.test.ts src/features/editor/engines/monaco/definition-link-scheduler.test.ts src/features/editor/engines/monaco/java-implementation-markers.test.ts
bun test src/features/editor/lsp/java-workspace-language-server.test.ts src/features/editor/lsp/java-workspace-change-scheduler.test.ts
bun test src/features/run

- name: Test shared Rust Core
if: needs.changes.outputs.rust_core == 'true'
Expand Down
54 changes: 54 additions & 0 deletions windows/tauri/src/features/run/components/run-output-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useMemo, useRef, type KeyboardEvent } from "react";
import { renderRunOutput } from "../utils/run-output-style";

export function RunOutputText({
source,
emptyLabel,
title,
}: {
source: string;
emptyLabel: string;
title: string;
}) {
const preRef = useRef<HTMLPreElement>(null);
const spans = useMemo(() => renderRunOutput(source), [source]);

const selectAll = () => {
const node = preRef.current;
const selection = window.getSelection();
if (!node || !selection) return;
const range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
};

const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== "a") return;
event.preventDefault();
event.stopPropagation();
selectAll();
};

return (
<div tabIndex={0} className="outline-none" onKeyDown={handleKeyDown}>
<div className="mb-1 font-medium text-subtle-foreground ui-text-sm">{title}</div>
{source ? (
<pre
ref={preRef}
className="cursor-text whitespace-pre-wrap font-mono text-[12px] text-foreground select-text *:select-text"
>
{spans.map((span, index) => (
<span key={index} className={span.className} style={span.style}>
{span.text}
</span>
))}
</pre>
) : (
<pre className="cursor-text whitespace-pre-wrap font-mono text-[12px] text-foreground select-text">
{emptyLabel}
</pre>
)}
</div>
);
}
10 changes: 6 additions & 4 deletions windows/tauri/src/features/run/components/run-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from "../utils/run-configuration";
import { RunConfigurationEditor } from "./run-configuration-editor";
import { JavaCupIcon, RunIcon } from "./run-icon";
import { RunOutputText } from "./run-output-text";

export default function RunPane() {
const { t } = useTranslation();
Expand Down Expand Up @@ -233,10 +234,11 @@ export default function RunPane() {
)}
</div>
<div className="min-h-0 flex-1 overflow-auto px-3 py-2">
<div className="mb-1 font-medium text-subtle-foreground ui-text-sm">{t("run.processOutput")}</div>
<pre className="whitespace-pre-wrap font-mono text-[12px] text-foreground">
{output || t("run.emptyOutput")}
</pre>
<RunOutputText
title={t("run.processOutput")}
source={output}
emptyLabel={t("run.emptyOutput")}
/>
</div>
{isSelectedRunning ? (
<RunStdinInput
Expand Down
19 changes: 19 additions & 0 deletions windows/tauri/src/features/run/stores/run.store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, mock, test } from "bun:test";

mock.module("@/platform/tauri-core", () => ({
invoke: mock(async () => undefined),
}));

const { createRunStore } = await import("./run.store");
const { PRIMARY_SESSION_ID } = await import("../types/run.types");

describe("run output session lifecycle", () => {
test("stop flushes a held prefix that never received a newline", async () => {
const store = createRunStore();
store.getState().actions.appendOutput(PRIMARY_SESSION_ID, "\u001b[32m");
expect(store.getState().primaryOutput).toBe("");
await store.getState().actions.stop(PRIMARY_SESSION_ID);
expect(store.getState().primaryOutput).toBe("\u001b[32m");
expect(store.getState().primaryRunning).toBe(false);
});
});
72 changes: 59 additions & 13 deletions windows/tauri/src/features/run/stores/run.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ import {
selectedToolchainCandidates,
} from "../utils/run-configuration";
import { editorSaveFailureMessage, runEditorSaveWorkflow } from "../services/run-editor-save";
import { createOutputStamper, trimRunOutput, type OutputStamper } from "../utils/output-timestamper";

const MAXIMUM_OUTPUT_CHARACTERS = 500_000;
const sessionWorkspaces = new Map<string, string>();
const outputStampers = new Map<string, OutputStamper>();

interface RunState {
root: string | null;
Expand Down Expand Up @@ -131,8 +133,28 @@ type ReadyRunState = Pick<
>;

function trimOutput(output: string): string {
if (output.length <= MAXIMUM_OUTPUT_CHARACTERS) return output;
return output.slice(output.length - MAXIMUM_OUTPUT_CHARACTERS);
return trimRunOutput(output, MAXIMUM_OUTPUT_CHARACTERS);
}

function stamperFor(sessionId: string): OutputStamper {
let stamper = outputStampers.get(sessionId);
if (!stamper) {
stamper = createOutputStamper();
outputStampers.set(sessionId, stamper);
}
return stamper;
}

function resetOutputStamper(sessionId: string): void {
stamperFor(sessionId).reset();
}

function appendStampedOutput(sessionId: string, existing: string, chunk: string): string {
return trimOutput(existing + stamperFor(sessionId).push(chunk));
Comment thread
1lck marked this conversation as resolved.
}

function flushStampedOutput(sessionId: string, existing: string): string {
return trimOutput(existing + stamperFor(sessionId).flush());
}

function optionsFromConfiguration(configuration: RunConfiguration): RunOptions {
Expand Down Expand Up @@ -379,6 +401,7 @@ export const createRunStore = () =>
const sessionId =
configuration.execution === "service" ? configuration.id : PRIMARY_SESSION_ID;
bindRunSessionWorkspace(sessionId);
resetOutputStamper(sessionId);
await stopRunProcess(sessionId).catch(() => undefined);
try {
const plan = await createLaunchPlan(root, configuration.id, currentFile);
Expand Down Expand Up @@ -454,22 +477,33 @@ export const createRunStore = () =>
const target = sessionId ?? get().selectedSessionId ?? PRIMARY_SESSION_ID;
await stopRunProcess(target).catch(() => undefined);
if (target === PRIMARY_SESSION_ID) {
set({ primaryRunning: false });
} else {
set((current) => ({
sessions: current.sessions.map((session) =>
session.id === target ? { ...session, isRunning: false } : session,
),
}));
set({
primaryRunning: false,
primaryOutput: flushStampedOutput(target, get().primaryOutput),
});
return;
}
set((current) => ({
sessions: current.sessions.map((session) =>
session.id === target
? {
...session,
isRunning: false,
output: flushStampedOutput(target, session.output),
}
: session,
),
}));
},

clearOutput: (sessionId) => {
const target = sessionId ?? get().selectedSessionId;
if (!target || target === PRIMARY_SESSION_ID) {
resetOutputStamper(PRIMARY_SESSION_ID);
set({ primaryOutput: "", primaryExitCode: null });
return;
}
resetOutputStamper(target);
set((current) => ({
sessions: current.sessions.map((session) =>
session.id === target ? { ...session, output: "", exitCode: null } : session,
Expand Down Expand Up @@ -537,26 +571,37 @@ export const createRunStore = () =>

appendOutput: (sessionId, chunk) => {
if (sessionId === PRIMARY_SESSION_ID) {
set({ primaryOutput: trimOutput(get().primaryOutput + chunk) });
set({ primaryOutput: appendStampedOutput(sessionId, get().primaryOutput, chunk) });
return;
}
set((current) => ({
sessions: current.sessions.map((session) =>
session.id === sessionId
? { ...session, output: trimOutput(session.output + chunk) }
? { ...session, output: appendStampedOutput(sessionId, session.output, chunk) }
: session,
),
}));
},

finishProcess: (sessionId, exitCode) => {
if (sessionId === PRIMARY_SESSION_ID) {
set({ primaryRunning: false, primaryExitCode: exitCode });
set({
primaryRunning: false,
primaryExitCode: exitCode,
primaryOutput: flushStampedOutput(sessionId, get().primaryOutput),
});
return;
}
set((current) => ({
sessions: current.sessions.map((session) =>
session.id === sessionId ? { ...session, isRunning: false, exitCode } : session,
session.id === sessionId
? {
...session,
isRunning: false,
exitCode,
output: flushStampedOutput(sessionId, session.output),
}
: session,
),
}));
},
Expand All @@ -576,6 +621,7 @@ export function runStoreForSession(sessionId: string) {

export function releaseRunSessionWorkspace(sessionId: string): void {
sessionWorkspaces.delete(sessionId);
outputStampers.delete(sessionId);
}

export function runOptionsFor(configuration: RunConfiguration): RunOptions {
Expand Down
Loading