KDevelop 是 KDE 社区出品的开源集成开发环境,支持 C/C++、Python、JavaScript、HTML/CSS、PHP、Java 等 30 余种编程语言,提供多标签编辑器、文件树管理、全局搜索、符号大纲、查找导航等功能。本文记录将其基于 Electron 壳方案适配到鸿蒙 PC 平台的完整流程。

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

AtomGit 仓库地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_kdevelop

一、技术架构分析

1.1 功能定位

KDevelop 的核心功能是代码编辑与项目管理。与 VS Code 等大型编辑器相比,它更加轻量,聚焦于多语言代码编辑、文件树管理和符号导航,适合在鸿蒙 PC 平台上快速浏览和编辑代码。

1.2 目标架构(鸿蒙 Electron)

  • 技术栈:Electron + HTML/CSS/JavaScript + CodeMirror 5.65.16 + 鸿蒙 web_engine 模块
  • 核心逻辑:主进程使用 Node.js 原生 fs/path 模块操作文件系统,渲染进程负责 UI 交互与代码编辑,通过 11 个 IPC 通道通信

1.3 架构设计

层级职责技术实现
主进程窗口管理 + 文件系统 IPCElectron BrowserWindow + Node.js fs/path
IPC 通道进程间通信(11 个通道)ipcMain.handle / ipcRenderer.invoke
渲染进程多标签编辑器 + 文件树 + 搜索CodeMirror 5.65.16 + 原生 DOM
样式层深色主题Catppuccin Mocha 配色方案

1.4 鸿蒙平台适配要点

鸿蒙 Electron 适配层存在三大兼容约束,必须在开发中严格遵守:

  • 禁止使用原生 prompt/confirm/alert 对话框,调用会导致 SubWindow 崩溃
  • 禁止使用原生 select 元素,调用会触发 SubWindow 崩溃
  • 禁止使用 setWindowOpenHandler 和 will-navigate API,调用会导致页面纯白

二、环境准备

2.1 开发环境要求

  • 操作系统:Windows 10/11
  • 开发工具:DevEco Studio(鸿蒙官方 IDE)
  • HarmonyOS SDK:API 21+(5.0.5+)
  • Node.js:v20+

2.2 项目结构

ohos_hap/
├── electron-apps/
│   └── KDevelop/               # KDevelop 应用源码
│       ├── main.js              # Electron 主进程(文件系统 IPC)
│       ├── renderer.js          # 渲染进程(多标签编辑器 + 文件树)
│       ├── index.html           # HTML 布局
│       ├── package.json         # 项目配置
│       └── styles/
│           └── kdevelop.css     # Catppuccin Mocha 深色主题
├── web_engine/                  # 鸿蒙 web_engine 模块
│   └── src/main/resources/
│       └── resfile/resources/app/  # 部署目录
└── build-profile.json5          # 鸿蒙构建配置

三、核心适配流程

3.1 第一步:创建主进程与文件系统 IPC 通道

文件:electron-apps/KDevelop/main.js

KDevelop 的核心是文件操作。在鸿蒙平台上,渲染进程无法直接使用 Node.js 的文件系统模块,必须通过 IPC 通道委托主进程完成。主进程注册 11 个 IPC 通道,覆盖文件对话框、文件读写、目录操作和文件搜索:

var electron = require('electron');
var app = electron.app;
var BrowserWindow = electron.BrowserWindow;
var screen = electron.screen;
var ipcMain = electron.ipcMain;
var dialog = electron.dialog;
var path = require('path');
var fs = require('fs');

app.disableHardwareAcceleration();

var mainWindow = null;

function createWindow() {
  try {
    var display = screen.getPrimaryDisplay();
    mainWindow = new BrowserWindow({
      width: Math.floor(display.workAreaSize.width * 0.92),
      height: Math.floor(display.workAreaSize.height * 0.88),
      frame: true,
      transparent: false,
      resizable: true,
      webPreferences: { nodeIntegration: true, contextIsolation: false }
    });
    mainWindow.loadFile('index.html');
  } catch (e) {
    console.warn('[KDevelop] 创建窗口失败:', e.message);
  }
}

app.whenReady().then(function() { createWindow(); });
app.on('window-all-closed', function() { app.quit(); });

文件对话框 IPC 通道注册(3 个通道):

ipcMain.handle('dialog:openFile', function() {
  return dialog.showOpenDialog(mainWindow, {
    properties: ['openFile'],
    filters: [{ name: '所有文件', extensions: ['*'] }]
  });
});

ipcMain.handle('dialog:openFolder', function() {
  return dialog.showOpenDialog(mainWindow, {
    properties: ['openDirectory']
  });
});

ipcMain.handle('dialog:saveFile', function() {
  return dialog.showSaveDialog(mainWindow, {
    filters: [{ name: '所有文件', extensions: ['*'] }]
  });
});

文件读写与目录操作 IPC 通道注册(7 个通道):

ipcMain.handle('file:read', function(event, filePath) {
  try {
    var content = fs.readFileSync(filePath, 'utf-8');
    return { success: true, content: content };
  } catch (e) {
    return { success: false, error: e.message };
  }
});

ipcMain.handle('file:write', function(event, filePath, content) {
  try {
    fs.writeFileSync(filePath, content, 'utf-8');
    return { success: true };
  } catch (e) {
    return { success: false, error: e.message };
  }
});

ipcMain.handle('dir:read', function(event, dirPath) {
  try {
    var entries = fs.readdirSync(dirPath, { withFileTypes: true });
    var items = [];
    for (var i = 0; i < entries.length; i++) {
      var entry = entries[i];
      if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
      items.push({
        name: entry.name,
        path: path.join(dirPath, entry.name),
        isDirectory: entry.isDirectory()
      });
    }
    items.sort(function(a, b) {
      if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
      return a.name.localeCompare(b.name);
    });
    return { success: true, items: items };
  } catch (e) {
    return { success: false, error: e.message };
  }
});

ipcMain.handle('file:create', function(event, filePath) {
  try {
    fs.writeFileSync(filePath, '', 'utf-8');
    return { success: true };
  } catch (e) {
    return { success: false, error: e.message };
  }
});

ipcMain.handle('dir:create', function(event, dirPath) {
  try {
    fs.mkdirSync(dirPath, { recursive: true });
    return { success: true };
  } catch (e) {
    return { success: false, error: e.message };
  }
});

ipcMain.handle('file:delete', function(event, filePath) {
  try {
    var stat = fs.statSync(filePath);
    if (stat.isDirectory()) {
      fs.rmdirSync(filePath, { recursive: true });
    } else {
      fs.unlinkSync(filePath);
    }
    return { success: true };
  } catch (e) {
    return { success: false, error: e.message };
  }
});

文件搜索 IPC 通道注册(1 个通道,支持正则匹配,最大深度 5 层,最多 200 条结果):

ipcMain.handle('file:search', function(event, dirPath, pattern) {
  try {
    var results = [];
    var regex;
    try { regex = new RegExp(pattern, 'i'); } catch (re) {
      return { success: false, error: '无效的正则表达式: ' + re.message, results: [] };
    }
    function walk(dir, depth) {
      if (depth > 5 || results.length > 200) return;
      var entries = fs.readdirSync(dir, { withFileTypes: true });
      for (var i = 0; i < entries.length; i++) {
        if (entries[i].name.startsWith('.') || entries[i].name === 'node_modules') continue;
        var fullPath = path.join(dir, entries[i].name);
        if (regex.test(entries[i].name)) {
          results.push({ name: entries[i].name, path: fullPath, isDirectory: entries[i].isDirectory() });
        }
        if (entries[i].isDirectory()) {
          walk(fullPath, depth + 1);
        }
      }
    }
    walk(dirPath, 0);
    return { success: true, results: results };
  } catch (e) {
    return { success: false, error: e.message, results: [] };
  }
});

关键要点

  • 11 个 IPC 通道覆盖文件对话框(3)、文件操作(5)、目录操作(2)、文件搜索(1)
  • dir:read 自动过滤隐藏文件和 node_modules 目录,目录排在文件前面
  • file:search 对无效正则表达式做 try/catch 保护,防止用户输入特殊字符导致崩溃
  • 递归搜索限制深度 5 层、结果数 200 条,防止大项目卡死

3.2 第二步:设计 IDE 多面板布局

文件:electron-apps/KDevelop/index.html

KDevelop 采用经典 IDE 布局:菜单栏 + 工具栏 + 左侧工具选择器 + 侧边栏 + 编辑器区域 + 状态栏。工具选择器使用垂直按钮组切换文件树、搜索、符号三个视图,这是 KDE 风格的关键设计:

<!-- 主体区域 -->
<div class="main">
  <!-- 左侧工具选择器(KDevelop 风格) -->
  <div class="tool-selector" id="toolSelector">
    <div class="tool-btn-v active" data-tool="files" title="文件/项目">
      <span class="tool-icon">📁</span>
    </div>
    <div class="tool-btn-v" data-tool="search" title="全局搜索">
      <span class="tool-icon">🔍</span>
    </div>
    <div class="tool-btn-v" data-tool="symbols" title="符号/大纲">
      <span class="tool-icon">𝑓</span>
    </div>
  </div>

  <!-- 侧边栏面板 -->
  <div class="sidebar" id="sidebar">
    <!-- 文件树视图 -->
    <div class="tool-view active" id="viewFiles">
      <div class="sidebar-header">
        <span class="sidebar-title">项目文件</span>
        <div class="sidebar-actions">
          <button class="sidebar-btn" id="btnNewFileTree" title="新建文件">📄</button>
          <button class="sidebar-btn" id="btnNewFolderTree" title="新建文件夹">📁</button>
          <button class="sidebar-btn" id="btnRefreshTree" title="刷新">🔄</button>
        </div>
      </div>
      <div class="sidebar-content" id="fileTree">
        <div class="sidebar-empty">
          <p>未打开文件夹</p>
          <button id="btnOpenFolderEmpty" class="btn-primary">打开文件夹</button>
        </div>
      </div>
    </div>
    <!-- 搜索视图 -->
    <div class="tool-view" id="viewSearch">
      <div class="sidebar-header">
        <span class="sidebar-title">全局搜索</span>
      </div>
      <div class="sidebar-content">
        <div class="search-bar">
          <input type="text" id="searchInput" class="search-input" placeholder="搜索文件名...">
          <button id="btnSearch" class="btn-search">🔍</button>
        </div>
        <div id="searchResults" class="search-results">
          <div class="sidebar-empty-hint">输入关键词搜索文件</div>
        </div>
      </div>
    </div>
    <!-- 符号/大纲视图 -->
    <div class="tool-view" id="viewSymbols">
      <div class="sidebar-header">
        <span class="sidebar-title">符号/大纲</span>
      </div>
      <div class="sidebar-content">
        <div id="symbolList" class="symbol-list">
          <div class="sidebar-empty-hint">打开文件后显示函数和类列表</div>
        </div>
      </div>
    </div>
  </div>

  <!-- 侧边栏拖动条 -->
  <div class="sidebar-resizer" id="sidebarResizer"></div>

  <!-- 编辑器区域 -->
  <div class="editor-area">
    <!-- 标签栏 -->
    <div class="tab-bar" id="tabBar">
      <div class="tab-bar-empty">欢迎使用 KDevelop</div>
    </div>
    <!-- 编辑器容器 -->
    <div class="editor-container" id="editorContainer">
      <div class="welcome-screen" id="welcomeScreen">
        <div class="welcome-logo">KDevelop</div>
        <div class="welcome-subtitle">KDE 集成开发环境</div>
        <div class="welcome-actions">
          <button class="welcome-btn" id="btnWelcomeOpen">📂 打开文件夹</button>
          <button class="welcome-btn" id="btnWelcomeNew">📄 新建文件</button>
        </div>
        <div class="welcome-info">
          <p>支持语言:C/C++、Python、JavaScript、HTML/CSSPHP、Java</p>
          <p>快捷键:Ctrl+N 新建 | Ctrl+O 打开 | Ctrl+S 保存 | Ctrl+F 查找</p>
        </div>
      </div>
    </div>
  </div>
</div>

CodeMirror CDN 加载 9 个语言模式和 8 个功能插件:

<!-- CodeMirror -->
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/htmlmixed/htmlmixed.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/xml/xml.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/javascript/javascript.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/css/css.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/python/python.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/clike/clike.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/php/php.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/markdown/markdown.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/shell/shell.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/edit/closebrackets.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/edit/closetag.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/fold/foldcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/fold/foldgutter.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/fold/brace-fold.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/fold/xml-fold.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/search/searchcursor.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/selection/active-line.min.js"></script>

关键要点

  • 工具选择器使用 3 个垂直按钮切换侧边栏视图,避免鸿蒙平台下拉框定位问题
  • 侧边栏与编辑器之间使用拖动条(sidebar-resizer)调整宽度
  • CodeMirror 通过 CDN 加载,支持 11 种语言模式映射和 8 个编辑辅助插件
  • 欢迎页提供打开文件夹和新建文件两个快捷入口

3.3 第三步:实现多标签 CodeMirror 编辑器

文件:electron-apps/KDevelop/renderer.js

渲染进程的核心逻辑包括:多标签编辑器、文件树管理、全局搜索、符号提取、查找栏和侧边栏拖动。

多标签编辑器:每个标签保存完整的 CodeMirror 实例和元数据,切换时显示/隐藏对应编辑器容器:

function createTab(name, filePath, content, lang) {
  var id = ++state.tabIdCounter;
  var tab = { id: id, name: name, path: filePath, modified: false, lang: lang };

  // 创建 CodeMirror 编辑器
  var container = document.createElement('div');
  container.id = 'editor-' + id;
  container.style.display = 'none';
  document.getElementById('editorContainer').appendChild(container);

  tab.editor = CodeMirror(container, {
    value: content || '',
    mode: getModeForLang(lang),
    theme: 'material-darker',
    lineNumbers: true,
    autoCloseBrackets: true,
    autoCloseTags: true,
    styleActiveLine: true,
    foldGutter: true,
    gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
    tabSize: 4,
    indentWithTabs: true
  });

  tab.editor.on('change', function() {
    if (!tab.modified) {
      tab.modified = true;
      updateTabUI(tab);
    }
  });

  tab.editor.on('cursorActivity', function() {
    if (state.activeTabId === id) updateStatusBar();
  });

  state.tabs.push(tab);
  renderTabBar();
  // 隐藏欢迎页
  var ws = document.getElementById('welcomeScreen');
  if (ws) ws.style.display = 'none';
  return tab;
}

function switchToTab(id) {
  state.activeTabId = id;
  state.tabs.forEach(function(tab) {
    var el = document.getElementById('editor-' + tab.id);
    if (el) el.style.display = tab.id === id ? '' : 'none';
  });
  var tab = getActiveTab();
  if (tab) {
    tab.editor.refresh();
    tab.editor.focus();
  }
  renderTabBar();
  updateStatusBar();
}

语言检测:支持 30 余种文件扩展名,映射到 11 种 CodeMirror 语言模式:

function detectLanguage(filename) {
  var ext = filename.split('.').pop().toLowerCase();
  var map = {
    'js': 'javascript', 'mjs': 'javascript', 'cjs': 'javascript',
    'ts': 'javascript', 'tsx': 'javascript',
    'html': 'html', 'htm': 'html', 'xml': 'xml', 'svg': 'xml',
    'css': 'css', 'scss': 'css', 'sass': 'css', 'less': 'css',
    'py': 'python', 'pyw': 'python',
    'cpp': 'c++', 'cxx': 'c++', 'cc': 'c++', 'c': 'c++', 'h': 'c++', 'hpp': 'c++',
    'java': 'java', 'kt': 'java', 'kts': 'java',
    'php': 'php',
    'md': 'markdown', 'markdown': 'markdown',
    'sh': 'shell', 'bash': 'shell', 'zsh': 'shell',
    'json': 'javascript', 'yaml': 'text', 'yml': 'text',
    'txt': 'text', 'log': 'text'
  };
  return map[ext] || 'text';
}

function getModeForLang(lang) {
  var map = {
    'javascript': 'javascript', 'html': 'htmlmixed', 'xml': 'xml',
    'css': 'css', 'python': 'python', 'c++': 'text/x-c++src',
    'java': 'text/x-java', 'php': 'php', 'markdown': 'markdown',
    'shell': 'shell', 'text': 'text'
  };
  return map[lang] || 'text';
}

符号提取:从当前编辑器内容提取函数、类和变量定义,显示在符号视图中,点击跳转到对应行:

function extractSymbols() {
  var tab = getActiveTab();
  var container = document.getElementById('symbolList');
  if (!tab || !tab.editor) {
    container.innerHTML = '<div class="sidebar-empty-hint">打开文件后显示函数和类列表</div>';
    return;
  }

  var symbols = [];
  var lines = tab.editor.getValue().split('\n');
  for (var i = 0; i < lines.length; i++) {
    var line = lines[i];
    // JavaScript/Python 函数
    var fnMatch = line.match(/(?:function\s+(\w+))|(?:const\s+(\w+)\s*=\s*(?:function|\())|(?:def\s+(\w+))|(?:async\s+function\s+(\w+))/);
    if (fnMatch) {
      var name = fnMatch[1] || fnMatch[2] || fnMatch[3] || fnMatch[4];
      symbols.push({ name: name, kind: 'fn', line: i + 1 });
    }
    // 类
    var classMatch = line.match(/(?:class\s+(\w+))/);
    if (classMatch) {
      symbols.push({ name: classMatch[1], kind: 'class', line: i + 1 });
    }
    // var/let/const 变量
    var varMatch = line.match(/(?:var|let|const)\s+(\w+)\s*=/);
    if (varMatch && !fnMatch) {
      symbols.push({ name: varMatch[1], kind: 'var', line: i + 1 });
    }
  }

  container.innerHTML = '';
  if (symbols.length === 0) {
    container.innerHTML = '<div class="sidebar-empty-hint">未检测到符号</div>';
    return;
  }
  symbols.forEach(function(sym) {
    var el = document.createElement('div');
    el.className = 'symbol-item';
    el.innerHTML = '<span class="symbol-kind ' + sym.kind + '">' + sym.kind.toUpperCase() + '</span><span>' + sym.name + '</span><span style="color:var(--overlay0);font-size:11px;margin-left:auto">:' + sym.line + '</span>';
    el.addEventListener('click', function() {
      tab.editor.setCursor(sym.line - 1, 0);
      tab.editor.focus();
    });
    container.appendChild(el);
  });
}

关键要点

  • 每个标签独立创建 CodeMirror 实例,通过 display 属性切换,避免销毁重建
  • detectLanguage 支持 30 余种扩展名,覆盖 C/C++、Python、JavaScript、TypeScript、HTML、CSS、PHP、Java、Shell、Markdown 等
  • extractSymbols 使用正则匹配函数、类和变量定义,点击符号项跳转到对应行
  • CodeMirror 配置 tabSize: 4 和 indentWithTabs: true,符合 C/C++ 编码规范

3.4 第四步:实现文件树与侧边栏

文件:electron-apps/KDevelop/renderer.js

文件树采用懒加载模式,点击文件夹时才读取子目录内容,避免一次性加载大项目:

function renderTreeItems(parent, items, basePath) {
  items.forEach(function(item) {
    if (item.isDirectory) {
      var folderEl = document.createElement('div');
      folderEl.className = 'tree-item';
      folderEl.innerHTML = '<span class="tree-toggle">▶</span><span class="tree-icon">📁</span><span class="tree-name">' + item.name + '</span>';
      folderEl.addEventListener('click', function(e) {
        e.stopPropagation();
        var children = folderEl.nextElementSibling;
        var toggle = folderEl.querySelector('.tree-toggle');
        if (children && children.classList.contains('tree-children')) {
          children.classList.toggle('collapsed');
          toggle.textContent = children.classList.contains('collapsed') ? '▶' : '▼';
        } else {
          toggle.textContent = '▼';
          folderEl.querySelector('.tree-icon').textContent = '📂';
          ipc('dir:read', item.path).then(function(res) {
            if (res.success) {
              var childContainer = document.createElement('div');
              childContainer.className = 'tree-children';
              renderTreeItems(childContainer, res.items, item.path);
              folderEl.parentNode.insertBefore(childContainer, folderEl.nextSibling);
            }
          });
        }
      });
      folderEl.addEventListener('contextmenu', function(e) {
        e.preventDefault();
        e.stopPropagation();
        state.contextTarget = item;
        showContextMenu(e.clientX, e.clientY);
      });
      parent.appendChild(folderEl);
    } else {
      var fileEl = document.createElement('div');
      fileEl.className = 'tree-item';
      fileEl.innerHTML = '<span class="tree-icon" style="margin-left:16px">' + getFileIcon(item.name) + '</span><span class="tree-name">' + item.name + '</span>';
      fileEl.addEventListener('click', function(e) {
        e.stopPropagation();
        document.querySelectorAll('.tree-item.active').forEach(function(a) { a.classList.remove('active'); });
        fileEl.classList.add('active');
        openFileByPath(item.path);
      });
      fileEl.addEventListener('contextmenu', function(e) {
        e.preventDefault();
        e.stopPropagation();
        state.contextTarget = item;
        showContextMenu(e.clientX, e.clientY);
      });
      parent.appendChild(fileEl);
    }
  });
}

侧边栏拖动条通过 mousedown/mousemove/mouseup 三事件实现宽度调整:

function initSidebarResizer() {
  var resizer = document.getElementById('sidebarResizer');
  var sidebar = document.getElementById('sidebar');
  var startX, startW;

  resizer.addEventListener('mousedown', function(e) {
    startX = e.clientX;
    startW = sidebar.offsetWidth;
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
    e.preventDefault();
  });

  function onMove(e) {
    var w = startW + (e.clientX - startX);
    if (w > 150 && w < 600) sidebar.style.width = w + 'px';
  }

  function onUp() {
    document.removeEventListener('mousemove', onMove);
    document.removeEventListener('mouseup', onUp);
  }
}

关键要点

  • 文件树懒加载:首次点击文件夹才通过 IPC 读取子目录,避免大项目一次性加载卡顿
  • 文件夹展开/折叠通过 tree-children 容器的 collapsed 类控制
  • 右键菜单支持新建文件、新建文件夹、删除操作
  • 侧边栏拖动条限制宽度范围 150px ~ 600px,防止拖出可视区域

3.5 第五步:Catppuccin Mocha 深色主题

文件:electron-apps/KDevelop/styles/kdevelop.css

KDevelop 采用 Catppuccin Mocha 配色方案,20 个标准 CSS 变量定义完整的深色主题:

:root {
  --base: #1e1e2e;
  --mantle: #181825;
  --crust: #11111b;
  --surface0: #313244;
  --surface1: #45475a;
  --surface2: #585b70;
  --text: #cdd6f4;
  --text-muted: #a6adc8;
  --overlay0: #6c7086;
  --blue: #89b4fa;
  --green: #a6e3a1;
  --red: #f38ba8;
  --yellow: #f9e2af;
  --mauve: #cba6f7;
  --peach: #fab387;
  --teal: #94e2d5;
  --sky: #89dceb;
  --pink: #f5c2e7;
  --border: #45475a;
  --border-light: #585b70;

  --menubar-height: 28px;
  --toolbar-height: 36px;
  --statusbar-height: 24px;
  --tool-selector-width: 40px;
  --sidebar-width: 260px;
  --font-mono: 'Consolas', 'Monaco', 'Courier New', monospace;
  --font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

工具选择器激活状态配色(KDevelop 风格左侧蓝色指示条):

.tool-btn-v {
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  border-radius: 6px;
  font-size: 16px;
  color: var(--overlay0);
  transition: background 0.15s, color 0.15s;
}

.tool-btn-v:hover { background: var(--surface0); color: var(--text); }
.tool-btn-v.active {
  background: var(--surface1);
  color: var(--blue);
  border-left: 3px solid var(--blue);
  border-radius: 0 6px 6px 0;
}

符号类型徽章配色(函数蓝色、类紫色、变量青色):

.symbol-kind {
  font-size: 10px;
  padding: 1px 4px;
  border-radius: 3px;
  font-weight: 600;
}

.symbol-kind.fn { background: rgba(137, 180, 250, 0.2); color: var(--blue); }
.symbol-kind.class { background: rgba(203, 166, 247, 0.2); color: var(--mauve); }
.symbol-kind.var { background: rgba(148, 226, 213, 0.2); color: var(--teal); }

关键要点

  • 20 个 Catppuccin Mocha 标准色 + 5 个布局变量,统一管理所有颜色和尺寸
  • 工具选择器激活按钮左侧 3px 蓝色边框,模拟 KDE 风格指示条
  • 符号类型使用半透明背景 + 实色文字,视觉层次清晰
  • 标签页修改后显示蓝色圆点标记(–peach 色),提示未保存状态

四、部署到鸿蒙平台

4.1 文件同步

将 Electron 应用文件复制到鸿蒙 web_engine 模块的部署目录:

$src = "electron-apps\KDevelop"
$dest = "web_engine\src\main\resources\resfile\resources\app"

Copy-Item "$src\main.js" "$dest\main.js" -Force
Copy-Item "$src\renderer.js" "$dest\renderer.js" -Force
Copy-Item "$src\index.html" "$dest\index.html" -Force
Copy-Item "$src\package.json" "$dest\package.json" -Force
Copy-Item "$src\styles\kdevelop.css" "$dest\styles\kdevelop.css" -Force

4.2 构建 HAP 包

在 DevEco Studio 中:

  1. 打开项目根目录
  2. 点击 Build > Build Hap(s)/APP(s)
  3. 选择 Build Hap(s)
  4. 等待构建完成

4.3 真机测试

  1. 连接鸿蒙设备(HUAWEI MateBook Pro)
  2. 点击 Run > Run ‘electron’
  3. 安装完成后,应用自动启动
  4. 点击欢迎页的打开文件夹按钮,选择项目目录验证文件树功能
  5. 点击文件打开编辑器,验证多标签和语法高亮

五、常见问题 FAQ

Q1:为什么文件操作要通过 IPC 而不是直接在渲染进程操作?

问题现象:渲染进程能否直接使用 Node.js 的 fs 模块读写文件?

根本原因:虽然 KDevelop 开启了 nodeIntegration: true,但鸿蒙 Electron 适配层对渲染进程的 Node.js 能力有限制,文件系统操作必须通过主进程 IPC 完成

解决方案:主进程注册 11 个 IPC 通道,渲染进程通过 ipcRenderer.invoke 调用

// 渲染进程通过 IPC 封装调用
function ipc(channel) {
  var args = Array.prototype.slice.call(arguments, 1);
  return ipcRenderer.invoke.apply(ipcRenderer, [channel].concat(args));
}

// 使用示例
ipc('file:read', filePath).then(function(result) {
  if (result.success) {
    var tab = createTab(name, filePath, result.content, lang);
    switchToTab(tab.id);
  }
});

优势:

  • 主进程统一管理文件操作,错误处理集中
  • 所有 IPC 通道返回统一格式 { success: true/false, … }
  • 渲染进程无需关心文件系统细节,代码更清晰

Q2:CodeMirror 为什么选择 5.x 而不是 6.x?

问题现象:CodeMirror 6 是最新版本,为什么不用?

分析:CodeMirror 6 采用模块化架构,需要打包工具(webpack/rollup)支持,而鸿蒙 Electron 壳应用使用纯 HTML 加载方式,无法使用打包工具

结论:CodeMirror 5.x 支持纯 CDN 加载,通过 script 标签直接引入,无需打包工具,适合鸿蒙平台

<!-- CodeMirror 5.x CDN 加载方式 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.css">
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/javascript/javascript.min.js"></script>

Q3:工具选择器为什么使用垂直按钮组?

问题现象:KDevelop 左侧的工具选择器为什么不用水平标签页?

根本原因:IDE 布局中侧边栏宽度有限,水平标签页文字会被截断,垂直按钮组使用图标更节省空间

解决方案:使用 40px 宽的垂直按钮栏,3 个图标按钮切换文件树、搜索、符号视图

<!-- 工具选择器(KDevelop 风格) -->
<div class="tool-selector" id="toolSelector">
  <div class="tool-btn-v active" data-tool="files" title="文件/项目">
    <span class="tool-icon">📁</span>
  </div>
  <div class="tool-btn-v" data-tool="search" title="全局搜索">
    <span class="tool-icon">🔍</span>
  </div>
  <div class="tool-btn-v" data-tool="symbols" title="符号/大纲">
    <span class="tool-icon">𝑓</span>
  </div>
</div>

优势:

  • 40px 宽度不占用编辑器空间
  • 图标按钮无需文字,国际化友好
  • 激活状态左侧蓝色边框,视觉指示清晰

Q4:文件树如何避免大项目卡顿?

问题现象:打开包含数千个文件的项目时,文件树是否会一次性全部加载?

分析:如果一次性加载所有文件和目录,会导致 IPC 传输大量数据,渲染进程 DOM 节点过多也会卡顿

解决方案:采用懒加载模式,首次只显示根目录,点击文件夹时才读取子目录

folderEl.addEventListener('click', function(e) {
  e.stopPropagation();
  var children = folderEl.nextElementSibling;
  var toggle = folderEl.querySelector('.tree-toggle');
  if (children && children.classList.contains('tree-children')) {
    // 已加载过,切换折叠状态
    children.classList.toggle('collapsed');
    toggle.textContent = children.classList.contains('collapsed') ? '▶' : '▼';
  } else {
    // 首次点击,通过 IPC 加载子目录
    toggle.textContent = '▼';
    ipc('dir:read', item.path).then(function(res) {
      if (res.success) {
        var childContainer = document.createElement('div');
        childContainer.className = 'tree-children';
        renderTreeItems(childContainer, res.items, item.path);
        folderEl.parentNode.insertBefore(childContainer, folderEl.nextSibling);
      }
    });
  }
});

Q5:键盘快捷键有哪些?

KDevelop 支持以下快捷键:

  • Ctrl + N:新建文件
  • Ctrl + O:打开文件
  • Ctrl + Shift + O:打开文件夹
  • Ctrl + S:保存
  • Ctrl + Shift + S:另存为
  • Ctrl + W:关闭标签
  • Ctrl + F:查找
  • Escape:关闭查找栏
function initKeyboardShortcuts() {
  document.addEventListener('keydown', function(e) {
    var ctrl = e.ctrlKey || e.metaKey;
    if (ctrl && e.key === 'n') { e.preventDefault(); newFile(); }
    else if (ctrl && e.key === 'o' && e.shiftKey) { e.preventDefault(); openFolder(); }
    else if (ctrl && e.key === 'o') { e.preventDefault(); openFile(); }
    else if (ctrl && e.key === 's' && e.shiftKey) { e.preventDefault(); saveFileAs(); }
    else if (ctrl && e.key === 's') { e.preventDefault(); saveCurrentFile(); }
    else if (ctrl && e.key === 'w') { e.preventDefault(); closeActiveTab(); }
    else if (ctrl && e.key === 'f') { e.preventDefault(); toggleFindBar(true); }
    else if (e.key === 'Escape') { toggleFindBar(false); }
  });
}

Q6:如何同步文件到鸿蒙项目?

问题现象:修改了 electron-apps/KDevelop/ 下的文件,但构建后没有生效

根本原因:文件没有同步到鸿蒙 web_engine 模块的部署目录

解决方案:使用 PowerShell 脚本同步文件

$src = "electron-apps\KDevelop"
$dest = "web_engine\src\main\resources\resfile\resources\app"

Copy-Item "$src\main.js" "$dest\main.js" -Force
Copy-Item "$src\renderer.js" "$dest\renderer.js" -Force
Copy-Item "$src\index.html" "$dest\index.html" -Force
Copy-Item "$src\package.json" "$dest\package.json" -Force
Copy-Item "$src\styles\kdevelop.css" "$dest\styles\kdevelop.css" -Force

部署路径:web_engine/src/main/resources/resfile/resources/app/

注意:每次修改代码后都需要同步,否则构建的 HAP 包不会包含最新代码。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐