AtCoder Code Template

AtCoderにコードのテンプレート機能を追加します。

Versão de: 24/06/2026. Veja: a última versão.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name            AtCoder Code Template
// @name:en         AtCoder Code Template
// @namespace       https://kiramku.f5.si/
// @version         1.0.0
// @description     AtCoderにコードのテンプレート機能を追加します。
// @description:en  We will add a code template feature to AtCoder.
// @author          kirameku
// @icon            https://atcoder.jp/favicon.ico
// @match           https://atcoder.jp/contests/*/tasks/*
// @match           https://atcoder.jp/contests/*/submit*
// @match           https://atcoder.jp/contests/*/custom_test*
// @grant           GM_setValue
// @grant           GM_getValue
// @license         MIT
// ==/UserScript==

(function() {
    'use strict';

    // 初期状態のテンプレート
    const defaultTemplates = [
        {
            name: "新規テンプレート",
            code: ``
        }
    ];

    // --- データ保存・取得用のヘルパー ---
    const getTemplates = () => {
        const saved = GM_getValue("atcoder_templates", null);
        return saved ? JSON.parse(saved) : defaultTemplates;
    };
    const saveTemplates = (templates) => GM_setValue("atcoder_templates", JSON.stringify(templates));
    const getSelectedIndex = () => GM_getValue("atcoder_selected_template_index", 0);
    const saveSelectedIndex = (index) => GM_setValue("atcoder_selected_template_index", index);
    const getAutoInsertTemplateName = () => GM_getValue("atcoder_auto_insert_template_name", "C++ 標準テンプレ");
    const saveAutoInsertTemplateName = (name) => GM_setValue("atcoder_auto_insert_template_name", name);
    const getAutoInsertConfig = () => GM_getValue("atcoder_auto_insert_enable", true);
    const saveAutoInsertConfig = (enable) => GM_setValue("atcoder_auto_insert_enable", enable);

    // 最後に自動挿入に成功したコードの履歴を保持(無駄な連続上書きを防ぐ)
    const getLastInsertedCode = () => GM_getValue("atcoder_last_inserted_code", "");
    const saveLastInsertedCode = (code) => GM_setValue("atcoder_last_inserted_code", code);

    const getCurrentTemplate = () => {
        const templates = getTemplates();
        const index = getSelectedIndex();
        return templates[index] || templates[0] || { name: "", code: "" };
    };

    const getAutoInsertTemplateCode = () => {
        const templates = getTemplates();
        const targetName = getAutoInsertTemplateName();
        const target = templates.find(t => t.name === targetName);
        return target ? target.code : (templates[0] ? templates[0].code : "");
    };

    // エディタ貼り付けロジック
    const setEditor = (s, force = false, retryCount = 0) => {
        const toggleButton = document.querySelector("button.btn-toggle-editor");
        const editor = document.querySelector("#plain-textarea");

        if (toggleButton && editor) {
            const currentVal = editor.value.trim();
            const lastInserted = getLastInsertedCode().trim();

            // 強制挿入(手動)でない場合、かつ、
            // 「完全に空」でもなく「前回自動挿入したコード」とも異なる(ユーザーが編集した形跡がある)場合はスキップ
            if (!force && currentVal !== "" && currentVal !== lastInserted) {
                return true;
            }

            try {
                toggleButton.click();
                editor.value = s;
                toggleButton.click();
                editor.value = s;
                editor.dispatchEvent(new Event('input', { bubbles: true }));

                if (editor.value.trim() === s.trim()) {
                    if (!force) saveLastInsertedCode(s); // 自動挿入時は履歴を保存
                    return true;
                }
            } catch (e) {
                console.error("Template insertion error:", e);
            }
        }

        if (retryCount < 10) {
            setTimeout(() => {
                setEditor(s, force, retryCount + 1);
            }, 200);
        }
        return false;
    };

    // UIを設置する関数
    const injectUI = () => {
        if (document.getElementById("atcoder-template-btn-row")) return;

        const mainEditor = document.getElementById("editor");
        if (!mainEditor) return;

        // 行コンテナ
        const rowContainer = document.createElement("div");
        rowContainer.id = "atcoder-template-btn-row";
        rowContainer.style.display = "block";
        rowContainer.style.width = "100%";
        rowContainer.style.marginBottom = "10px";
        rowContainer.style.clear = "both";
        rowContainer.style.position = "relative";
        rowContainer.style.zIndex = "10";

        // 1. テンプレート選択ドロップダウン
        const dropdownGroup = document.createElement("div");
        dropdownGroup.className = "btn-group";
        dropdownGroup.style.marginRight = "5px";
        dropdownGroup.style.verticalAlign = "middle";

        const mainDropBtn = document.createElement("button");
        mainDropBtn.type = "button";
        mainDropBtn.className = "btn btn-default btn-sm dropdown-toggle";
        mainDropBtn.setAttribute("data-toggle", "dropdown");

        const listIcon = document.createElement("span");
        listIcon.className = "glyphicon glyphicon-list";
        listIcon.setAttribute("aria-hidden", "true");
        mainDropBtn.appendChild(listIcon);

        const btnTextNode = document.createTextNode("");
        mainDropBtn.appendChild(document.createTextNode(" "));
        mainDropBtn.appendChild(btnTextNode);

        const caret = document.createElement("span");
        caret.className = "caret";
        caret.style.marginLeft = "5px";
        mainDropBtn.appendChild(caret);

        const menuList = document.createElement("ul");
        menuList.className = "dropdown-menu";

        const updateDropdownMenu = () => {
            menuList.innerHTML = "";
            const templates = getTemplates();
            const currentIndex = getSelectedIndex();

            const currentTpl = templates[currentIndex] || templates[0] || { name: "テンプレなし" };
            btnTextNode.nodeValue = currentTpl.name + " ";

            templates.forEach((tpl, idx) => {
                const li = document.createElement("li");
                const a = document.createElement("a");
                a.href = "javascript:void(0);";
                a.innerText = tpl.name;

                a.addEventListener("click", (e) => {
                    e.preventDefault();
                    saveSelectedIndex(idx);
                    updateDropdownMenu();
                });

                li.appendChild(a);
                menuList.appendChild(li);
            });
        };
        updateDropdownMenu();

        dropdownGroup.appendChild(mainDropBtn);
        dropdownGroup.appendChild(menuList);

        // 2. 手動貼り付けボタン(forceフラグをtrueにして強制上書き)
        const pasteBtn = document.createElement("button");
        pasteBtn.id = "atcoder-template-paste-btn";
        pasteBtn.type = "button";
        pasteBtn.className = "btn btn-default btn-sm";
        pasteBtn.style.marginRight = "5px";

        const pasteIcon = document.createElement("span");
        pasteIcon.className = "glyphicon glyphicon-paste";
        pasteIcon.setAttribute("aria-hidden", "true");
        pasteBtn.appendChild(pasteIcon);
        pasteBtn.appendChild(document.createTextNode(" 貼り付け"));
        pasteBtn.addEventListener("click", () => setEditor(getCurrentTemplate().code, true));

        // 3. 設定モーダルを開くボタン
        const configBtn = document.createElement("button");
        configBtn.id = "atcoder-template-modal-btn";
        configBtn.type = "button";
        configBtn.className = "btn btn-default btn-sm";

        const configIcon = document.createElement("span");
        configIcon.className = "glyphicon glyphicon-cog";
        configIcon.setAttribute("aria-hidden", "true");
        configBtn.appendChild(configIcon);
        configBtn.appendChild(document.createTextNode(" テンプレ設定"));

        rowContainer.appendChild(dropdownGroup);
        rowContainer.appendChild(pasteBtn);
        rowContainer.appendChild(configBtn);

        mainEditor.parentNode.insertBefore(rowContainer, mainEditor);

        // 4. モーダル本体の作成
        if (!document.getElementById("atcoder-template-modal-overlay")) {
            const modalOverlay = document.createElement("div");
            modalOverlay.id = "atcoder-template-modal-overlay";
            modalOverlay.style.position = "fixed";
            modalOverlay.style.top = "0";
            modalOverlay.style.left = "0";
            modalOverlay.style.width = "100%";
            modalOverlay.style.height = "100%";
            modalOverlay.style.backgroundColor = "rgba(0, 0, 0, 0.5)";
            modalOverlay.style.zIndex = "9999";
            modalOverlay.style.display = "none";
            modalOverlay.style.justifyContent = "center";
            modalOverlay.style.alignItems = "center";

            const modalBody = document.createElement("div");
            modalBody.style.backgroundColor = "#fff";
            modalBody.style.padding = "20px";
            modalBody.style.borderRadius = "6px";
            modalBody.style.width = "620px";
            modalBody.style.maxWidth = "95%";
            modalBody.style.boxShadow = "0 4px 15px rgba(0,0,0,0.2)";

            const title = document.createElement("h4");
            title.innerText = "テンプレート管理・設定";
            title.style.marginTop = "0";
            title.style.borderBottom = "1px solid #eee";
            title.style.paddingBottom = "10px";

            const configRow = document.createElement("div");
            configRow.style.display = "flex";
            configRow.style.alignItems = "center";
            configRow.style.flexWrap = "wrap";
            configRow.style.gap = "15px";
            configRow.style.marginBottom = "15px";

            const checkboxContainer = document.createElement("div");
            checkboxContainer.style.display = "flex";
            checkboxContainer.style.alignItems = "center";
            const checkbox = document.createElement("input");
            checkbox.type = "checkbox";
            checkbox.id = "auto-insert-checkbox";
            checkbox.style.marginRight = "8px";
            const checkboxLabel = document.createElement("label");
            checkboxLabel.htmlFor = "auto-insert-checkbox";
            checkboxLabel.innerText = "ページアクセス時に自動で貼り付ける";
            checkboxLabel.style.marginBottom = "0";
            checkboxContainer.appendChild(checkbox);
            checkboxContainer.appendChild(checkboxLabel);

            const autoSelectContainer = document.createElement("div");
            autoSelectContainer.style.display = "flex";
            autoSelectContainer.style.alignItems = "center";
            autoSelectContainer.style.gap = "5px";

            const autoSelectLabel = document.createElement("label");
            autoSelectLabel.innerText = "自動貼り付けテンプレ:";
            autoSelectLabel.style.marginBottom = "0";
            autoSelectLabel.style.fontSize = "12px";

            const autoDropGroup = document.createElement("div");
            autoDropGroup.className = "btn-group";

            const autoDropBtn = document.createElement("button");
            autoDropBtn.type = "button";
            autoDropBtn.className = "btn btn-default btn-xs dropdown-toggle";
            autoDropBtn.setAttribute("data-toggle", "dropdown");
            autoDropBtn.style.minWidth = "130px";
            autoDropBtn.style.textAlign = "left";
            autoDropBtn.style.display = "flex";
            autoDropBtn.style.justifyContent = "space-between";
            autoDropBtn.style.alignItems = "center";

            const autoDropText = document.createTextNode("");
            autoDropBtn.appendChild(autoDropText);
            const autoCaret = document.createElement("span");
            autoCaret.className = "caret";
            autoDropBtn.appendChild(autoCaret);

            const autoDropMenu = document.createElement("ul");
            autoDropMenu.className = "dropdown-menu";

            autoDropGroup.appendChild(autoDropBtn);
            autoDropGroup.appendChild(autoDropMenu);

            autoSelectContainer.appendChild(autoSelectLabel);
            autoSelectContainer.appendChild(autoDropGroup);

            configRow.appendChild(checkboxContainer);
            configRow.appendChild(autoSelectContainer);

            const mainFlex = document.createElement("div");
            mainFlex.style.display = "flex";
            mainFlex.style.gap = "15px";

            const leftPane = document.createElement("div");
            leftPane.style.width = "190px";
            leftPane.style.display = "flex";
            leftPane.style.flexDirection = "column";

            const listLabel = document.createElement("label");
            listLabel.innerText = "テンプレ一覧:";
            leftPane.appendChild(listLabel);

            const tplListDiv = document.createElement("div");
            tplListDiv.style.width = "100%";
            tplListDiv.style.height = "210px";
            tplListDiv.style.border = "1px solid #ccc";
            tplListDiv.style.borderRadius = "4px";
            tplListDiv.style.overflowY = "auto";
            tplListDiv.style.backgroundColor = "#fff";
            tplListDiv.style.marginBottom = "8px";
            leftPane.appendChild(tplListDiv);

            const ioBtnArea = document.createElement("div");
            ioBtnArea.style.display = "flex";
            ioBtnArea.style.gap = "5px";

            const exportBtn = document.createElement("button");
            exportBtn.type = "button";
            exportBtn.className = "btn btn-default btn-xs";
            exportBtn.style.flexGrow = "1";
            const exportIcon = document.createElement("span");
            exportIcon.className = "glyphicon glyphicon-export";
            exportBtn.appendChild(exportIcon);
            exportBtn.appendChild(document.createTextNode(" 書き出し"));

            const importBtn = document.createElement("button");
            importBtn.type = "button";
            importBtn.className = "btn btn-default btn-xs";
            importBtn.style.flexGrow = "1";
            const importIcon = document.createElement("span");
            importIcon.className = "glyphicon glyphicon-import";
            importBtn.appendChild(importIcon);
            importBtn.appendChild(document.createTextNode(" 読み込み"));

            const fileInput = document.createElement("input");
            fileInput.type = "file";
            fileInput.accept = ".json";
            fileInput.style.display = "none";

            ioBtnArea.appendChild(exportBtn);
            ioBtnArea.appendChild(importBtn);
            ioBtnArea.appendChild(fileInput);
            leftPane.appendChild(ioBtnArea);

            const rightPane = document.createElement("div");
            rightPane.style.flexGrow = "1";
            rightPane.style.display = "flex";
            rightPane.style.flexDirection = "column";

            const actionBtnArea = document.createElement("div");
            actionBtnArea.style.display = "flex";
            actionBtnArea.style.justifyContent = "flex-end";
            actionBtnArea.style.gap = "5px";
            actionBtnArea.style.marginBottom = "8px";

            const addBtn = document.createElement("button");
            addBtn.type = "button";
            addBtn.className = "btn btn-success btn-xs";
            const addIcon = document.createElement("span");
            addIcon.className = "glyphicon glyphicon-plus";
            addBtn.appendChild(addIcon);
            addBtn.appendChild(document.createTextNode(" 新規追加"));

            const delBtn = document.createElement("button");
            delBtn.type = "button";
            delBtn.className = "btn btn-danger btn-xs";
            const delIcon = document.createElement("span");
            delIcon.className = "glyphicon glyphicon-trash";
            delBtn.appendChild(delIcon);
            delBtn.appendChild(document.createTextNode(" 削除"));

            actionBtnArea.appendChild(addBtn);
            actionBtnArea.appendChild(delBtn);

            const nameLabel = document.createElement("label");
            nameLabel.innerText = "テンプレート名:";
            const nameInput = document.createElement("input");
            nameInput.type = "text";
            nameInput.className = "form-control input-sm";
            nameInput.style.marginBottom = "10px";

            const codeLabel = document.createElement("label");
            codeLabel.innerText = "コード内容:";
            const codeTextarea = document.createElement("textarea");
            codeTextarea.style.width = "100%";
            codeTextarea.style.height = "185px";
            codeTextarea.style.fontFamily = "monospace";
            codeTextarea.style.fontSize = "12px";
            codeTextarea.style.padding = "5px";
            codeTextarea.style.boxSizing = "border-box";

            rightPane.appendChild(actionBtnArea);
            rightPane.appendChild(nameLabel);
            rightPane.appendChild(nameInput);
            rightPane.appendChild(codeLabel);
            rightPane.appendChild(codeTextarea);

            mainFlex.appendChild(leftPane);
            mainFlex.appendChild(rightPane);

            const btnArea = document.createElement("div");
            btnArea.style.textAlign = "right";
            btnArea.style.marginTop = "15px";

            const cancelBtn = document.createElement("button");
            cancelBtn.type = "button";
            cancelBtn.className = "btn btn-default btn-sm";
            cancelBtn.style.marginRight = "10px";
            cancelBtn.innerText = "キャンセル";

            const saveBtn = document.createElement("button");
            saveBtn.type = "button";
            saveBtn.className = "btn btn-primary btn-sm";
            const saveIcon = document.createElement("span");
            saveIcon.className = "glyphicon glyphicon-save";
            saveBtn.appendChild(saveIcon);
            saveBtn.appendChild(document.createTextNode(" 設定を保存"));

            btnArea.appendChild(cancelBtn);
            btnArea.appendChild(saveBtn);

            modalBody.appendChild(title);
            modalBody.appendChild(configRow);
            modalBody.appendChild(mainFlex);
            modalBody.appendChild(btnArea);
            modalOverlay.appendChild(modalOverlay.cloneNode(false));
            modalOverlay.innerHTML = "";
            modalOverlay.appendChild(modalBody);
            document.body.appendChild(modalOverlay);

            let editingTemplates = [];
            let currentEditIdx = 0;
            let selectedAutoName = "";

            const refreshAutoDropdown = () => {
                autoDropMenu.innerHTML = "";
                autoDropText.nodeValue = selectedAutoName || "(選択なし) ";

                editingTemplates.forEach(t => {
                    const li = document.createElement("li");
                    const a = document.createElement("a");
                    a.href = "javascript:void(0);";
                    a.innerText = t.name;
                    a.addEventListener("click", (e) => {
                        e.preventDefault();
                        selectedAutoName = t.name;
                        refreshAutoDropdown();
                    });
                    li.appendChild(a);
                    autoDropMenu.appendChild(li);
                });
            };

            const refreshModalList = () => {
                tplListDiv.innerHTML = "";
                editingTemplates.forEach((t, i) => {
                    const item = document.createElement("div");
                    item.innerText = t.name || "(無名のテンプレ)";
                    item.style.padding = "6px 10px";
                    item.style.cursor = "pointer";
                    item.style.fontSize = "13px";
                    item.style.borderBottom = "1px solid #eee";
                    item.style.userSelect = "none";

                    if (i === currentEditIdx) {
                        item.style.backgroundColor = "#337ab7";
                        item.style.color = "#fff";
                    } else {
                        item.style.backgroundColor = "#fff";
                        item.style.color = "#333";
                        item.addEventListener("mouseenter", () => item.style.backgroundColor = "#f5f5f5");
                        item.addEventListener("mouseleave", () => item.style.backgroundColor = "#fff");
                    }

                    item.addEventListener("click", () => {
                        currentEditIdx = i;
                        refreshModalList();
                    });

                    tplListDiv.appendChild(item);
                });

                if (editingTemplates[currentEditIdx]) {
                    nameInput.value = editingTemplates[currentEditIdx].name;
                    codeTextarea.value = editingTemplates[currentEditIdx].code;
                    nameInput.disabled = false;
                    codeTextarea.disabled = false;
                } else {
                    nameInput.value = "";
                    codeTextarea.value = "";
                    nameInput.disabled = true;
                    codeTextarea.disabled = true;
                }

                refreshAutoDropdown();
            };

            nameInput.addEventListener("input", () => {
                if (editingTemplates[currentEditIdx]) {
                    const oldName = editingTemplates[currentEditIdx].name;
                    editingTemplates[currentEditIdx].name = nameInput.value;
                    const activeItem = tplListDiv.children[currentEditIdx];
                    if (activeItem) activeItem.innerText = nameInput.value || "(無名のテンプレ)";
                    if (selectedAutoName === oldName) { selectedAutoName = nameInput.value; }
                    refreshAutoDropdown();
                }
            });
            codeTextarea.addEventListener("input", () => {
                if (editingTemplates[currentEditIdx]) {
                    editingTemplates[currentEditIdx].code = codeTextarea.value;
                }
            });

            addBtn.addEventListener("click", () => {
                editingTemplates.push({
                    name: "新規テンプレート " + (editingTemplates.length + 1),
                    code: "// ここにコードを記述"
                });
                currentEditIdx = editingTemplates.length - 1;
                refreshModalList();
                tplListDiv.scrollTop = tplListDiv.scrollHeight;
            });

            delBtn.addEventListener("click", () => {
                if (editingTemplates.length <= 1) {
                    alert("最低でも1つのテンプレートは残す必要があります。");
                    return;
                }
                const deletedName = editingTemplates[currentEditIdx].name;
                editingTemplates.splice(currentEditIdx, 1);
                currentEditIdx = Math.max(0, currentEditIdx - 1);
                if (selectedAutoName === deletedName) {
                    selectedAutoName = editingTemplates[0] ? editingTemplates[0].name : "";
                }
                refreshModalList();
            });

            exportBtn.addEventListener("click", () => {
                const dataStr = JSON.stringify(editingTemplates, null, 2);
                const blob = new Blob([dataStr], { type: "application/json" });
                const url = URL.createObjectURL(blob);
                const a = document.createElement("a");
                a.href = url;
                a.download = "atcoder_templates.json";
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
            });

            importBtn.addEventListener("click", () => fileInput.click());
            fileInput.addEventListener("change", (e) => {
                const file = e.target.files[0];
                if (!file) return;
                const reader = new FileReader();
                reader.onload = (event) => {
                    try {
                        const parsed = JSON.parse(event.target.result);
                        if (Array.isArray(parsed) && parsed.length > 0) {
                            if (confirm("読み込んだテンプレート一覧に差し替えますか?")) {
                                editingTemplates = parsed;
                                currentEditIdx = 0;
                                selectedAutoName = parsed[0].name;
                                refreshModalList();
                            }
                        }
                    } catch (err) { alert("失敗しました。"); }
                    fileInput.value = "";
                };
                reader.readAsText(file);
            });

            configBtn.addEventListener("click", () => {
                checkbox.checked = getAutoInsertConfig();
                editingTemplates = JSON.parse(JSON.stringify(getTemplates()));
                currentEditIdx = Math.min(getSelectedIndex(), editingTemplates.length - 1);
                if (currentEditIdx < 0) currentEditIdx = 0;
                selectedAutoName = getAutoInsertTemplateName();
                refreshModalList();
                modalOverlay.style.display = "flex";
            });

            const closeModal = () => { modalOverlay.style.display = "none"; };
            cancelBtn.addEventListener("click", closeModal);
            modalOverlay.addEventListener("click", (e) => { if (e.target === modalOverlay) closeModal(); });

            saveBtn.addEventListener("click", () => {
                saveTemplates(editingTemplates);
                saveAutoInsertConfig(checkbox.checked);
                saveAutoInsertTemplateName(selectedAutoName);

                let selIdx = getSelectedIndex();
                if (selIdx >= editingTemplates.length) {
                    saveSelectedIndex(0);
                }
                updateDropdownMenu();
                closeModal();
                alert("テンプレート一覧を保存しました!");
            });
        }
    };

    let lastInsertedUrl = "";

    // 監視処理
    const observer = new MutationObserver(() => {
        const currentUrl = window.location.href;
        const toggleButton = document.querySelector("button.btn-toggle-editor");
        const editor = document.querySelector("#plain-textarea");

        if (toggleButton && editor) {
            injectUI();

            if (lastInsertedUrl !== currentUrl) {
                if (getAutoInsertConfig()) {
                    setTimeout(() => {
                        // forceフラグをfalseにして自動挿入を実行
                        setEditor(getAutoInsertTemplateCode(), false);
                    }, 150);
                }
                lastInsertedUrl = currentUrl;
            }
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });
})();