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
14 changes: 13 additions & 1 deletion apps/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as vscode from "vscode";
import * as path from "path";
import { fetchAtCoderProblem, fetchAtCoderTasks } from "./atcoder";
import { CfError, ProxyError, LoginRequiredError, setSessionCookie, fetchSubStatus } from "./tools/fetch";
import { CfError, ProxyError, LoginRequiredError, setSessionCookie, fetchSubStatus, fetchSubmitHistory } from "./tools/fetch";
import { fetchContest, signedUpContest } from "./tools/SignUpContest";
import { translateTextRaw } from "./tools/deepl";
import { runCommand } from "./tools/command";
Expand Down Expand Up @@ -240,6 +240,18 @@ export async function handleSubmitCode(
}
}

export async function handleFetchSubHistory(contest: string, send: (payload: Record<string, unknown>) => void) {
send({ type: "loading", text: `正在获取 ${contest} 提交记录...` });
try {
const submissions = await fetchSubmitHistory(contest);
send({ type: "submissionHistory", submissions });
} catch (error) {
if (!handleErrorWithCfAndLogin(error, send)) {
send({ type: "error", text: error instanceof Error ? error.message : "获取提交记录失败" });
}
}
}

async function pullSubmitStatu(contest: string, taskName: string, send: (payload: Record<string, unknown>) => void,): Promise<void> {
const maxSetp = 15;
for (let i = 1; i <= maxSetp; i++) {
Expand Down
36 changes: 11 additions & 25 deletions apps/vscode-extension/src/tools/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,16 @@ import {
handleRegistration,
handleFetchSubmitPage,
handleSubmitCode,
handleFetchSubHistory,
} from "../extension";

const loadCommands = new Set(["loadContest", "loadProblem", "openBrowser"]);
const deeplCommands = new Set(["translate", "setApiKey"]);
const cookieCommands = new Set(["getCookie", "setCookie"]);
const problemCommands = new Set(["registerContest", "copyMarkdown", "alert"]);
const submitCommands = new Set(["fetchSubmitPage", "submitCode"]);
const submitCommands = new Set(["fetchSubmitPage", "submitCode", "fetchSubmissionHistory", "fetchSubmissionDetail"]);

export async function runCommand(
message: IncomingMessage,
context: vscode.ExtensionContext,
sendToWebview: (payload: Record<string, unknown>) => void,
) {
export async function runCommand(message: IncomingMessage, context: vscode.ExtensionContext, sendToWebview: (payload: Record<string, unknown>) => void,) {
if (loadCommands.has(message.command!)) await runLoadCommand(message, sendToWebview);
else if (deeplCommands.has(message.command!)) await runDeepL(message, context, sendToWebview);
else if (cookieCommands.has(message.command!)) await runCookie(message, context, sendToWebview);
Expand All @@ -41,16 +38,16 @@ async function runSubmit(command: IncomingMessage, context: vscode.ExtensionCont
if (!command.contest) return false;
await handleSubmitCode(command.contest, command.taskScreenName, command.languageId, command.sourceCode, sendToWebview);
return true;
case "fetchSubmissionHistory":
if (!command.contest) return false;
await handleFetchSubHistory(command.contest, sendToWebview);
return true;
default:
return false;
}
}

async function runProblem(
command: IncomingMessage,
context: vscode.ExtensionContext,
sendToWebview: (payload: Record<string, unknown>) => void,
): Promise<boolean> {
async function runProblem(command: IncomingMessage, context: vscode.ExtensionContext, sendToWebview: (payload: Record<string, unknown>) => void,): Promise<boolean> {
switch (command.command) {
case "registerContest":
if (!command.contest) return false;
Expand All @@ -71,11 +68,7 @@ async function runProblem(
}
}

async function runCookie(
command: IncomingMessage,
context: vscode.ExtensionContext,
sendToWebview: (payload: Record<string, unknown>) => void,
): Promise<boolean> {
async function runCookie(command: IncomingMessage, context: vscode.ExtensionContext, sendToWebview: (payload: Record<string, unknown>) => void,): Promise<boolean> {
switch (command.command) {
case "getCookie":
await handleGetCookie(context, sendToWebview);
Expand All @@ -88,11 +81,7 @@ async function runCookie(
}
}

async function runDeepL(
command: IncomingMessage,
context: vscode.ExtensionContext,
sendToWebview: (payload: Record<string, unknown>) => void,
): Promise<boolean> {
async function runDeepL(command: IncomingMessage, context: vscode.ExtensionContext, sendToWebview: (payload: Record<string, unknown>) => void,): Promise<boolean> {
switch (command.command) {
case "translate":
await handleTranslate(command.payload, command.targetLang, context, sendToWebview);
Expand All @@ -108,10 +97,7 @@ async function runDeepL(
}
}

async function runLoadCommand(
command: IncomingMessage,
sendToWebview: (payload: Record<string, unknown>) => void,
): Promise<boolean> {
async function runLoadCommand(command: IncomingMessage, sendToWebview: (payload: Record<string, unknown>) => void,): Promise<boolean> {
switch (command.command) {
case "loadContest":
if (!command.contest) return false;
Expand Down
41 changes: 39 additions & 2 deletions apps/vscode-extension/src/tools/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as stream from "stream";
import * as zlib from "zlib";
import * as net from "net";
import * as tls from "tls";
import { SubRecord } from "./types"


export class CfError extends Error {
Expand Down Expand Up @@ -279,7 +280,7 @@ function handleResponse(
}

export function fetchText(url: string): Promise<string> {
const logPrefix = `[fetchText]`;
const logPrefix = `[fetchText]`;

const savedProxy = saveProxyEnv();

Expand Down Expand Up @@ -368,4 +369,40 @@ export async function fetchSubStatus(contest: string): Promise<Map<string, strin
now.set(tasks[1], statuMatch[2].trim());
}
return now;
}
}

export async function fetchSubmitHistory(contest: string): Promise<SubRecord[]> {
const html = await fetchText(`https://atcoder.jp/contests/${contest}/submissions/me`);
const records: SubRecord[] = [];
const reg = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
let rowMatch: RegExpExecArray | null;
for (; (rowMatch = reg.exec(html)) !== null;) {
const rowHtml = rowMatch[1];
const timeMatch = rowHtml.match(/<td[^>]*class="text-center"[^>]*>([\s\S]*?)<\/td>/i);
if (!timeMatch) continue;
const time = timeMatch[1].trim().replace(/<[^>]+>/g, "");

const taskLinkMatch = rowHtml.match(/href="\/contests\/[^/]+\/tasks\/([^"#?]+)"[^>]*>([^<]+)</i);
if (!taskLinkMatch) continue;
const taskScreenName = taskLinkMatch[1];
const task = taskLinkMatch[2].trim();

const langMatch = rowHtml.match(/<td[^>]*class="text-center"[^>]*>[\s\S]*?<\/td>\s*<td[^>]*>([^<]*)<\/td>/i);
const language = langMatch ? langMatch[1].trim() : "";

const scoreMatch = rowHtml.match(/<td[^>]*class="text-right"[^>]*>\s*(\d+)\s*<\/td>/i);
const score = scoreMatch ? scoreMatch[1] : "0";

const statusMatch = rowHtml.match(/<span[^>]*class=(["'])[^"']*\blabel\b[^"']*\1[^>]*>\s*([^<]+)\s*<\/span>/i);
if (!statusMatch) continue;
const status = statusMatch[2].trim();

const detailMatch = rowHtml.match(/<a[^>]*href="\/contests\/[^/]+\/submissions\/(\d+)"[^>]*>/i);
if (!detailMatch) continue;
const id = detailMatch[1];

records.push({ id, time, task, taskScreenName, language, score, status });
}
return records;
}

13 changes: 12 additions & 1 deletion apps/vscode-extension/src/tools/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,20 @@ export interface IncomingMessage {
problem?: AtCoderProblem;
}

export interface SubRecord {
id: string;
time: string;
task: string;
taskScreenName: string; // 题目标识
language: string; // 编程语言
score: string; // 分数
status: string;
}

export interface SubStatus {
label: string;
value: string;
url: string;
status?: string;
}
}

109 changes: 104 additions & 5 deletions packages/webview/src/WebviewApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ const WebviewApp: React.FC<WebviewAppProps> = ({
const [sourceCode, setSourceCode] = React.useState("");
const [submitResult, setSubmitResult] = React.useState<SubmitResult | null>(null);
const [copiedSample, setCopiedSample] = React.useState<Record<string, boolean>>({});
const [showSubmissionHistory, setShowSubmissionHistory] = React.useState(false);
const [submissionHistory, setSubmissionHistory] = React.useState<Array<{ id: string; time: string; task: string; taskScreenName: string; language: string; score: string; status: string }>>([]);
const [loadingHistory, setLoadingHistory] = React.useState(false);

const loadContest = async (nextContest: string) => {
setIsLoading(true);
Expand Down Expand Up @@ -92,6 +95,12 @@ const WebviewApp: React.FC<WebviewAppProps> = ({
setTimeout(() => setCopiedSample(prev => ({ ...prev, [key]: false })), 1500);
};

const handleFetchSubmissionHistory = () => {
setLoadingHistory(true);
setStatus("正在获取提交记录...");
vscode.postMessage({ command: "fetchSubmissionHistory", contest } as unknown as WebviewMessage);
};

const handleSubmitCode = () => {
if (!selectedSubmitTask || !selectedSubmitLanguage || !sourceCode.trim()) return;
setSubmitResult(null);
Expand Down Expand Up @@ -199,6 +208,13 @@ const WebviewApp: React.FC<WebviewAppProps> = ({
const statuses = message.statuses ?? {};
setTasks(prev => prev.map(t => ({ ...t, status: statuses[t.value] })));
}
if (message.type === "submissionHistory") {
const m = message as any;
setSubmissionHistory(m.submissions ?? []);
setLoadingHistory(false);
setStatus(`已获取 ${(m.submissions ?? []).length} 条提交记录`);
setShowSubmissionHistory(true);
}
};

window.addEventListener("message", handleMessage);
Expand Down Expand Up @@ -335,6 +351,22 @@ const WebviewApp: React.FC<WebviewAppProps> = ({
>
提交代码
</Button>
<Button
onClick={() => {
if (submissionHistory.length === 0) {
handleFetchSubmissionHistory();
} else {
setShowSubmissionHistory(!showSubmissionHistory);
}
}}
disabled={isLoading}
variant={showSubmissionHistory ? "secondary" : "primary"}
size="sm"
className="h-[28px] text-[12px]"
title="提交记录"
>
提交记录
</Button>
</div>
{Rated && (
<label className="flex items-center gap-1 text-[12px] select-none cursor-pointer">
Expand Down Expand Up @@ -456,6 +488,66 @@ const WebviewApp: React.FC<WebviewAppProps> = ({
</div>
)}

{showSubmissionHistory && (
<div className="border-b border-[var(--vscode-panel-border)] bg-[var(--vscode-textBlockQuote-background)]">
<div className="p-3 space-y-3">
<div className="flex items-center justify-between">
<div className="text-[12px] font-semibold">{contest} 提交记录</div>
<div className="flex gap-2">
<Button
size="sm"
onClick={handleFetchSubmissionHistory}
disabled={loadingHistory}
className="h-[24px] text-[11px]"
>
{loadingHistory ? "刷新中..." : "刷新"}
</Button>
<Button
size="sm"
variant="secondary"
onClick={() => { setShowSubmissionHistory(false); }}
className="h-[24px] text-[11px]"
>
关闭
</Button>
</div>
</div>

{submissionHistory.length === 0 ? (
<div className="text-[12px] opacity-60">
{loadingHistory ? "正在获取提交记录..." : "暂无提交记录"}
</div>
) : (
<div className="space-y-1 max-h-[300px] overflow-y-auto">
{submissionHistory.map((s) => (
<div key={s.id} className="flex items-center gap-2 px-2 py-1.5 text-[12px] border border-[var(--vscode-panel-border)] rounded hover:bg-[var(--vscode-list-hoverBackground)]">
<span className="text-[11px] opacity-60 w-[120px] flex-shrink-0">{s.time}</span>
<span className="flex-1 truncate">{s.task}</span>
<span className={`text-[10px] px-1 rounded font-bold ${
s.status === "AC" ? "text-green-500 bg-green-500/10" :
s.status === "WA" ? "text-red-500 bg-red-500/10" :
s.status === "TLE" ? "text-cyan-500 bg-cyan-500/10" :
s.status === "MLE" ? "text-yellow-500 bg-yellow-500/10" :
s.status === "RE" ? "text-purple-500 bg-purple-500/10" :
s.status === "CE" ? "text-gray-400 bg-gray-400/10" :
"text-gray-400 bg-gray-400/10"
}`}>{s.status}</span>
<span className="text-[11px] opacity-60 w-[50px] text-right">{s.score}</span>
<a
href="#"
onClick={(e) => { e.preventDefault(); vscode.postMessage({ command: "openBrowser", url: `https://atcoder.jp/contests/${contest}/submissions/${s.id}` }); }}
className="text-[11px] underline opacity-60 hover:opacity-100 flex-shrink-0"
>
查看
</a>
</div>
))}
</div>
)}
</div>
</div>
)}

<div className="flex-1 overflow-y-auto p-3 space-y-3">
{tasks.length > 0 && (
<Card className="p-3 space-y-2">
Expand All @@ -472,11 +564,18 @@ const WebviewApp: React.FC<WebviewAppProps> = ({
}}
className="flex items-center gap-1"
>
{task.status === "AC"
? <span className="text-green-500 font-bold">✓</span>
: task.status
? <span className="text-red-500 font-bold">✗</span>
: null}
{task.status && (
<span className={`text-[10px] px-1 rounded font-bold ${
task.status === "AC" ? "text-green-500 bg-green-500/10" :
task.status === "WA" ? "text-red-500 bg-red-500/10" :
task.status === "TLE" ? "text-cyan-500 bg-cyan-500/10" :
task.status === "MLE" ? "text-yellow-500 bg-yellow-500/10" :
task.status === "RE" ? "text-purple-500 bg-purple-500/10" :
task.status === "CE" ? "text-gray-400 bg-gray-400/10" :
task.status === "WJ" || task.status === "WR" ? "text-yellow-500 bg-yellow-500/10" :
"text-gray-400 bg-gray-400/10"
}`}>{task.status}</span>
)}
{task.label}
</Button>
))}
Expand Down
13 changes: 12 additions & 1 deletion packages/webview/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,19 @@ export interface SubmitResult {
url?: string;
}

export interface SubmissionRecord {
id: string;
time: string;
task: string;
taskScreenName: string;
language: string;
score: string;
status: string;
}

export interface WebviewMessage {
type?: string;
command?: 'alert' | 'error' | 'loadContest' | 'loadProblem' | 'openBrowser' | 'translate' | 'setApiKey' | 'setCookie' | 'getCookie' | 'loginRequired' | 'registerContest' | 'copyMarkdown' | 'fetchSubmitPage' | 'submitCode';
command?: 'alert' | 'error' | 'loadContest' | 'loadProblem' | 'openBrowser' | 'translate' | 'setApiKey' | 'setCookie' | 'getCookie' | 'loginRequired' | 'registerContest' | 'copyMarkdown' | 'fetchSubmitPage' | 'submitCode' | 'fetchSubmissionHistory';
statusMessage?: string;
text?: string;
contest?: string;
Expand All @@ -48,6 +58,7 @@ export interface WebviewMessage {
csrfToken?: string;
submitResult?: SubmitResult;
statuses?: Record<string, string>;
submissions?: SubmissionRecord[];
}

export interface VSCodeAPI {
Expand Down
Binary file modified release/extension.vsix
Binary file not shown.
Loading