You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Ace Editor+Pyodide的Python编辑器:实现目录展示与标签页

实现Pyodide编辑器的文件目录侧边栏和顶部标签页功能

一、左侧展示当前目录文件列表

1. 修改HTML结构

添加侧边栏容器,调整整体布局适配新结构:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Pyodide in Action</title>
    <link rel="stylesheet" href="styles.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.4/ace.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.4/ext-static_highlight.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.4/ext-modelist.js"></script>
    <script src="https://unpkg.com/file-saver"></script>
    <script src="pyodide.js"></script>
</head>

<body>
    <!-- 侧边栏:目录文件列表 -->
    <div id="file-sidebar">
        <h3>文件目录</h3>
        <ul id="file-list"></ul>
    </div>

    <!-- 顶部标签栏 -->
    <div id="tab-bar">
        <div id="tab-container"></div>
    </div>

    <div class="container">
        <div class="editor-wrapper">
            <pre id="editor"></pre>
        </div>
    </div>

    <button id="run_code">运行代码</button><br>
    <br>
    <textarea rows=10 cols=30 id=output style="margin: 0px; width: calc(100% - 220px); height: 461px;">Loading and compiling...</textarea><br>

    <script type="text/javascript" src="challenge-editor.js"></script>
</body>
</html>

2. 调整CSS样式

添加侧边栏、标签栏样式,修改原有布局适配新结构:

/* 侧边栏样式 */
#file-sidebar {
    position: fixed;
    left: 0;
    top: 0;
    width: 200px;
    height: 100vh;
    background-color: #f5f5f5;
    border-right: 1px solid #ddd;
    padding: 15px;
    overflow-y: auto;
}

#file-sidebar h3 {
    margin-top: 0;
    font-size: 16px;
    border-bottom: 1px solid #ddd;
    padding-bottom: 8px;
}

#file-list {
    list-style: none;
    padding: 0;
    margin: 0;
}

#file-list li {
    padding: 8px 5px;
    cursor: pointer;
    border-radius: 4px;
    margin-bottom: 4px;
}

#file-list li:hover {
    background-color: #e0e0e0;
}

/* 顶部标签栏样式 */
#tab-bar {
    position: fixed;
    left: 200px;
    top: 0;
    right: 0;
    height: 40px;
    background-color: #f5f5f5;
    border-bottom: 1px solid #ddd;
    display: flex;
    align-items: center;
    padding: 0 10px;
    overflow-x: auto;
}

#tab-container {
    display: flex;
    gap: 4px;
}

.tab-item {
    background-color: #e0e0e0;
    padding: 8px 15px;
    border-radius: 4px 4px 0 0;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 14px;
}

.tab-item.active {
    background-color: #fff;
    border-bottom: 1px solid #fff;
}

.tab-close {
    font-size: 12px;
    color: #666;
}

.tab-close:hover {
    color: #000;
}

/* 主内容区调整 */
.container {
    margin-left: 200px;
    margin-top: 40px;
    width: calc(100% - 220px);
    height: calc(100vh - 120px);
    padding: 10px;
}

.editor-wrapper {
    width: 100%;
    height: 100%;
    position: relative;
}

#editor { 
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

#run_code {
    margin-left: 200px;
    border-radius: 4px;
    padding: 10px 20px;
    background-color: #408ddf;
    color: white;
    font-size: 16px;
    border: none;
    cursor: pointer;
}

#output {
    margin-left: 200px;
}

3. 扩展JavaScript功能

添加目录加载、标签页管理逻辑:

const fileSaver = require('file-saver');

var editor = ace.edit("editor");
var output_pane;
// 维护当前打开的文件状态
var openFiles = {};
var activeFilePath = '/_static/test_files/main.py';

languagePluginLoader.then(() => {
    appendOutput('Python ready.\n');
});

function appendOutput(msg) {
    output_pane.value = output_pane.value + '\n' + msg;
    output_pane.scrollTop = output_pane.scrollHeight;
}

function configEditor(){
    editor = ace.edit("editor");
    editor.setTheme("ace/theme/xcode");
    editor.session.setMode("ace/mode/python");
    editor.setShowPrintMargin(false);
    editor.setBehavioursEnabled(true);
    editor.setFontSize(13);
    // 先加载目录,再打开初始文件
    loadDirectory(getDirPath(activeFilePath));
    openCode(activeFilePath);
}

// 解析文件路径的目录部分
function getDirPath(filePath) {
    return filePath.substring(0, filePath.lastIndexOf('/'));
}

// 加载指定目录下的文件列表
async function loadDirectory(dirPath) {
    try {
        // 若使用Pyodide虚拟文件系统,可替换为pyodide.FS.readdir(dirPath)
        const response = await fetch(`${dirPath}/?list`);
        const files = await response.json();
        
        const fileListEl = document.getElementById('file-list');
        fileListEl.innerHTML = '';
        
        files.forEach(file => {
            if (file.endsWith('.py')) {
                const li = document.createElement('li');
                li.textContent = file;
                li.addEventListener('click', () => {
                    openCode(`${dirPath}/${file}`);
                });
                fileListEl.appendChild(li);
            }
        });
    } catch (error) {
        console.error('加载目录失败:', error);
        appendOutput('加载目录失败,请检查目录路径配置');
    }
}

function openCode(filePath) {
    // 如果文件已打开,直接切换标签
    if (openFiles[filePath]) {
        switchToTab(filePath);
        return;
    }

    fetch(filePath)
      .then(response => response.text())
      .then(code => {
        var modelist = ace.require("ace/ext/modelist");
        var modeName = modelist.getModeForPath(filePath).mode;
        
        // 保存文件状态
        openFiles[filePath] = {
            code: code,
            mode: modeName,
            fileName: filePath.substring(filePath.lastIndexOf('/') + 1)
        };

        // 创建标签页
        createTab(filePath);
        // 切换到该标签
        switchToTab(filePath);
      })
      .catch(error => {
        console.error('打开文件失败:', error);
        appendOutput(`打开文件${filePath}失败`);
      });
}

// 创建标签页元素
function createTab(filePath) {
    const tabContainer = document.getElementById('tab-container');
    const tabItem = document.createElement('div');
    tabItem.className = 'tab-item';
    tabItem.dataset.path = filePath;
    
    const fileNameEl = document.createElement('span');
    fileNameEl.textContent = openFiles[filePath].fileName;
    
    const closeBtn = document.createElement('span');
    closeBtn.className = 'tab-close';
    closeBtn.textContent = '×';
    closeBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        closeTab(filePath);
    });
    
    tabItem.appendChild(fileNameEl);
    tabItem.appendChild(closeBtn);
    tabItem.addEventListener('click', () => {
        switchToTab(filePath);
    });
    
    tabContainer.appendChild(tabItem);
}

// 切换到指定标签页
function switchToTab(filePath) {
    // 更新标签激活状态
    document.querySelectorAll('.tab-item').forEach(tab => {
        tab.classList.remove('active');
        if (tab.dataset.path === filePath) {
            tab.classList.add('active');
        }
    });

    // 更新编辑器内容
    const file = openFiles[filePath];
    editor.session.setMode(file.mode);
    editor.session.setValue(file.code);
    activeFilePath = filePath;

    // 监听编辑器内容变化,实时更新文件状态
    editor.session.on('change', () => {
        openFiles[filePath].code = editor.getValue();
    });
}

// 关闭标签页
function closeTab(filePath) {
    // 如果是最后一个标签,不允许关闭
    if (Object.keys(openFiles).length <= 1) {
        appendOutput('至少保留一个打开的文件');
        return;
    }

    // 移除标签元素
    document.querySelector(`.tab-item[data-path="${filePath}"]`).remove();
    // 删除文件状态
    delete openFiles[filePath];

    // 切换到第一个打开的文件
    const newActivePath = Object.keys(openFiles)[0];
    switchToTab(newActivePath);
}

async function runCode(code_to_run) {
    console.logs = [];

    try {
        await window.pyodide.runPython(code_to_run);
        appendOutput(console.logs.join('\n')); 
        save_code(code_to_run);
    } catch (err) {
        alert("Error: " + err);
    }
}

function save_code(code) {
    var blob = new Blob([code], { type: "text/plain;charset=utf-8" });
    window.saveAs(blob, activeFilePath);
}

document.addEventListener('DOMContentLoaded', (event) => {
    output_pane = document.getElementById("output");

    document.getElementById("run_code").addEventListener('click', function () {
        runCode(editor.getValue());
    });

    console.stdlog = console.log.bind(console);
    console.logs = [];
    console.log = function(){
        console.logs.push(Array.from(arguments).join(' '));
        console.stdlog.apply(console, arguments);
    }
    
    configEditor();
});

关键说明

  • 目录加载:若使用Pyodide虚拟文件系统,可将loadDirectory中的fetch逻辑替换为pyodide.FS.readdir(dirPath),无需后端接口支持。
  • 内容同步:编辑器内容变化时会实时更新openFiles中的存储,切换标签时自动同步最新内容。

二、顶部文件标签页功能说明

上述代码已完整实现VS Code风格的标签页功能,核心逻辑:

  • 通过openFiles对象存储所有打开文件的内容、语法模式和文件名
  • 打开新文件时自动创建对应标签,点击标签切换编辑器内容
  • 点击标签关闭按钮可移除标签,自动切换到其他打开的文件
  • 激活标签会高亮显示,区分当前编辑文件

内容的提问来源于stack exchange,提问作者Cr3

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 09:10:00