Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c8e5f94
feat: runFilesでのリアルタイムdiagnostic出力機能およびエディタ表示を追加
na-trium-144 Aug 18, 2026
db8ef44
refactor: DiagnosticをframesベースのN:1構造に変更し、diagnostics単体テストをfileExecuti…
na-trium-144 Aug 24, 2026
d039d61
pythonのテストを修正
na-trium-144 Aug 24, 2026
1eb160d
Merge remote-tracking branch 'origin/main' into feat/runtime-diagnostics
na-trium-144 Aug 24, 2026
cfa2490
rubyのテストを修正
na-trium-144 Aug 26, 2026
f44c535
diagnosticディレクトリを削除
na-trium-144 Aug 28, 2026
763d5f8
python,rubyのエラーを構造化したオブジェクトとして返す
na-trium-144 Aug 28, 2026
bec2a89
frameの順序を統一
na-trium-144 Aug 28, 2026
1e9160e
Merge remote-tracking branch 'origin/main' into feat/runtime-diagnostics
na-trium-144 Aug 29, 2026
4b9c080
C++,Rustのdiagnostic出力に対応
na-trium-144 Aug 29, 2026
66ab789
outputの出力と、regex修正
na-trium-144 Aug 30, 2026
6e0b85a
標準ライブラリフレームの除外
na-trium-144 Aug 30, 2026
3d298fc
jsのスタックトレースをパース
na-trium-144 Aug 30, 2026
6dcb7d2
tscのエラー修正
na-trium-144 Aug 30, 2026
9251724
replの場合はmain.jsじゃないほうがいいな
na-trium-144 Aug 30, 2026
bd9ad09
format
na-trium-144 Aug 30, 2026
080ef62
fix redos
na-trium-144 Aug 30, 2026
1af9232
fix redos again
na-trium-144 Aug 30, 2026
085c6ae
format
na-trium-144 Aug 30, 2026
39eef6c
fix
na-trium-144 Aug 31, 2026
c198dc9
エラーの見た目を調整
na-trium-144 Sep 1, 2026
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
82 changes: 80 additions & 2 deletions app/terminal/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { lazy, Suspense, useEffect, useState } from "react";
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
import clsx from "clsx";
import { useChangeTheme } from "@/themeToggle";
import { useEmbedContext } from "./embedContext";
Expand Down Expand Up @@ -41,7 +41,83 @@ interface EditorProps {
}
export function EditorComponent(props: EditorProps) {
const theme = useChangeTheme();
const { files, writeFile } = useEmbedContext();
const { files, writeFile, diagnostics } = useEmbedContext();
const fileDiagnostics = useMemo(
() =>
Object.values(diagnostics)
.flat()
.filter((diag) =>
diag.frames.some((f) => f.filename === props.filename)
),
[diagnostics, props.filename]
);

const annotations = useMemo(() => {
return fileDiagnostics.flatMap((diag) =>
diag.frames
.slice(0, 1)
.filter((f) => f.filename === props.filename)
.map((f) => ({
row: Math.max(0, f.startLineNumber - 1),
column: Math.max(0, (f.startColumn ?? 1) - 1),
text: diag.message,
type: diag.severity ?? "error", // "error" | "warning" | "info"
}))
);
}, [fileDiagnostics, props.filename]);

const markers = useMemo(() => {
return fileDiagnostics.flatMap((diag) =>
diag.frames
.map((f, i) => ({ ...f, isFirstFrame: i === 0 }))
.filter((f) => f.filename === props.filename)
.map((f) => {
const startRow = Math.max(0, f.startLineNumber - 1);
const endRow = f.endLineNumber
? Math.max(startRow, f.endLineNumber - 1)
: startRow;
const startCol =
f.startColumn !== undefined ? Math.max(0, f.startColumn - 1) : 0;
const endCol =
f.endColumn !== undefined
? Math.max(startCol + 1, f.endColumn - 1)
: Number.MAX_SAFE_INTEGER;

const isError = (diag.severity ?? "error") === "error";
const isWarning = diag.severity === "warning";
const className = clsx(
"absolute rounded-b-none! border-dashed border-b-1",
isError
? "border-error"
: isWarning
? "border-warning"
: "border-accent",
f.isFirstFrame &&
(isError
? "bg-error/20"
: isWarning
? "bg-warning/20"
: "bg-accent/20")
);

return {
startRow,
startCol,
endRow,
endCol,
className,
type:
f.startColumn !== undefined &&
f.endColumn !== undefined &&
startRow === endRow
? ("text" as const)
: ("fullLine" as const),
inFront: false,
};
})
);
}, [fileDiagnostics, props.filename]);

const code = files[props.filename] || props.initContent;
useEffect(() => {
if (!files[props.filename] && props.initContent) {
Expand Down Expand Up @@ -207,6 +283,8 @@ export function EditorComponent(props: EditorProps) {
value={code}
onChange={(code: string) => writeFile({ [props.filename]: code })}
setOptions={{ useWorker: false }}
annotations={annotations}
markers={markers}
/>
</Suspense>
) : (
Expand Down
41 changes: 40 additions & 1 deletion app/terminal/embedContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
"use client";

import { ReplCommand, ReplOutput } from "@my-code/runtime/interface";
import {
Diagnostic,
ReplCommand,
ReplOutput,
} from "@my-code/runtime/interface";
import {
createContext,
ReactNode,
Expand Down Expand Up @@ -40,6 +44,10 @@ interface IEmbedContext {
execResults: Readonly<Record<Filename, ReplOutput[]>>;
clearExecResult: (filename: Filename) => void;
addExecOutput: (filename: Filename, output: ReplOutput) => void;

diagnostics: Readonly<Record<Filename, Diagnostic[]>>;
clearDiagnostics: (filename?: Filename) => void;
addDiagnostic: (filename: Filename, diagnostic: Diagnostic) => void;
}
const EmbedContext = createContext<IEmbedContext>(null!);

Expand Down Expand Up @@ -80,11 +88,15 @@ export function EmbedContextProvider({
const [execResults, setExecResults] = useState<
Record<Filename, ReplOutput[]>
>({});
const [diagnostics, setDiagnostics] = useState<
Record<Filename, Diagnostic[]>
>({});
if (pageKey && pageKey !== prevPageKey) {
setPrevPageKey(pageKey);
setReplOutputs({});
setCommandIdCounters({});
setExecResults({});
setDiagnostics({});
}

const writeFile = useCallback(
Expand Down Expand Up @@ -181,6 +193,30 @@ export function EmbedContextProvider({
[]
);

const clearDiagnostics = useCallback(
(filename?: Filename) =>
setDiagnostics((diags) => {
if (filename !== undefined) {
const next = { ...diags };
delete next[filename];
return next;
}
return {};
}),
[]
);
const addDiagnostic = useCallback(
(filename: Filename, diagnostic: Diagnostic) =>
setDiagnostics((diags) => {
const current = diags[filename] ? [...diags[filename]] : [];
return {
...diags,
[filename]: [...current, diagnostic],
};
}),
[]
);

return (
<EmbedContext.Provider
value={{
Expand All @@ -192,6 +228,9 @@ export function EmbedContextProvider({
execResults,
clearExecResult,
addExecOutput,
diagnostics,
clearDiagnostics,
addDiagnostic,
}}
>
{children}
Expand Down
60 changes: 38 additions & 22 deletions app/terminal/exec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,14 @@ export function ExecFile(props: ExecProps) {
}
},
});
const { files, clearExecResult, addExecOutput, writeFile } =
useEmbedContext();
const {
files,
clearExecResult,
addExecOutput,
writeFile,
clearDiagnostics,
addDiagnostic,
} = useEmbedContext();

if (props.language.runtime === undefined) {
throw new Error(
Expand All @@ -94,29 +100,37 @@ export function ExecFile(props: ExecProps) {
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
const filenameKey = props.filenames.join(",");
clearExecResult(filenameKey);
clearDiagnostics(filenameKey);
setContents("");
let isFirstOutput = true;
await runFiles(props.filenames, files, (output) => {
if (output.type === "file") {
writeFile({ [output.filename]: output.content });
return;
}
addExecOutput(filenameKey, output);
if (isFirstOutput) {
// Clear "実行中です..." message only on first output
clearTerminal(terminalInstanceRef.current!);
isFirstOutput = false;
await runFiles(
props.filenames,
files,
(output) => {
if (output.type === "file") {
writeFile({ [output.filename]: output.content });
return;
}
addExecOutput(filenameKey, output);
if (isFirstOutput) {
// Clear "実行中です..." message only on first output
clearTerminal(terminalInstanceRef.current!);
isFirstOutput = false;
}
// Append only the new output
writeOutput(
terminalInstanceRef.current!,
output,
undefined,
null, // ファイル実行で"return"メッセージが返ってくることはないはずなので、Prismを渡す必要はない
props.language
);
setContents((prev) => prev + output.message + "\n");
},
(diagnostic) => {
addDiagnostic(filenameKey, diagnostic);
}
// Append only the new output
writeOutput(
terminalInstanceRef.current!,
output,
undefined,
null, // ファイル実行で"return"メッセージが返ってくることはないはずなので、Prismを渡す必要はない
props.language
);
setContents((prev) => prev + output.message + "\n");
});
);
setExecutionState("idle");
if (isFirstOutput) {
// If there was no output, clear the "実行中です..." message
Expand All @@ -132,6 +146,8 @@ export function ExecFile(props: ExecProps) {
clearExecResult,
addExecOutput,
writeFile,
clearDiagnostics,
addDiagnostic,
terminalInstanceRef,
props.language,
files,
Expand Down
11 changes: 11 additions & 0 deletions packages/jsEval/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
export { replLikeEval } from "./eval";
export { checkSyntax } from "./syntax";
export { createReplConsole } from "./console";
export {
parseStackTrace,
formatStackTrace,
findSyntaxErrorLine,
parseError,
} from "./stackTrace";
export type { ConsoleOutput, ConsoleEmitter, ReplConsole } from "./console";
export type {
ParsedStackFrame,
DiagnosticFrameInfo,
ParsedErrorInfo,
} from "./stackTrace";
Loading
Loading