代码折叠(Code Folding)允许开发者隐藏代码的特定区域,只显示整体结构。本文详细介绍区域折叠、缩进折叠、标记折叠等多种实现方案。
折叠区域定义
代码折叠需要先识别可折叠的区域:
/* 折叠区域定义 */
interface FoldRange {
start: number; // 起始行
end: number; // 结束行
level: number; // 折叠层级
type: string; // 类型:function/class/if/for/...
name?: string; // 名称(如函数名)*/
FoldState = 'expanded' | 'collapsed' | 'always_expanded';
}
/* 计算所有可折叠区域 */
function computeFoldRanges(text) {
const lines = text.split('\n');
const ranges = [];
for (let i = 0; i < lines.length; i++) {
const range = detectFoldRange(lines, i);
if (range) {
ranges.push(range);
i = range.end - 1; // 跳过已处理的行
}
}
return ranges;
}
基于缩进的折叠
最基础的折叠方式,根据缩进级别自动识别可折叠区域:
/* 基于缩进的折叠计算 */
function computeIndentFolds(lines) {
const folds = [];
const stack = []; // 存储缩进级别和对应行号
for (let i = 0; i < lines.length; i++) {
const indent = getIndentLevel(lines[i]);
/* 处理空白行 */
if (lines[i].trim() === '') continue;
/* 当前缩进小于栈顶,说明之前的区域该闭合了 */
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
const popped = stack.pop();
popped.endLine = i;
/* 只有跨行区域才能折叠 */
if (popped.endLine - popped.startLine > 1) {
folds.push(popped);
}
}
/* 当前行作为新区域的开始 */
stack.push({
startLine: i,
endLine: lines.length,
indent: indent
});
}
return folds;
}
function getIndentLevel(line) {
const match = line.match(^(\s*));
return match ? match[1].length : 0;
}
基于语法的折叠
更智能的方式,根据代码语法结构识别可折叠区域:
/* 语法感知的折叠 */
const FOLD_TOKENS = {
'{': { type: 'open', name: 'brace' },
'}': { type: 'close', name: 'brace' },
'function': { type: 'open', name: 'function' },
'class': { type: 'open', name: 'class' },
'if': { type: 'open', name: 'if' },
'for': { type: 'open', name: 'for' },
'while': { type: 'open', name: 'while' },
'switch': { type: 'open', name: 'switch' }
};
function computeSyntaxFolds(tokens) {
const folds = [];
const stack = [];
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (FOLD_TOKENS[token.type]) {
const config = FOLD_TOKENS[token.type];
if (config.type === 'open') {
stack.push({
type: config.name,
start: token.line,
name: getFoldName(token, tokens)
});
} else if (config.type === 'close' && stack.length > 0) {
const open = stack.pop();
open.end = token.line;
/* 只保留有实际内容的区域 */
if (open.end - open.start > 0) {
folds.push(open);
}
}
}
}
return folds;
}
function getFoldName(token, allTokens) {
/* 提取函数名/类名等作为折叠显示名称 */
const nextToken = allTokens[allTokens.indexOf(token) + 1];
if (nextToken && ('identifier'.includes(nextToken.type))) {
return nextToken.value;
}
return token.type;
}
折叠状态管理
管理每个折叠区域的展开/收起状态:
/* 折叠管理器 */
class FoldManager {
constructor(editor) {
this.editor = editor;
this.foldRanges = []; // 所有可折叠区域
this.collapsedLines = new Set(); // 当前收起的行
}
/* 初始化折叠区域 */
init() {
const text = this.editor.getText();
this.foldRanges = computeFoldRanges(text);
}
/* 切换折叠状态 */
toggle(lineNumber) {
const fold = this.getFoldAtLine(lineNumber);
if (!fold) return;
if (this.collapsedLines.has(fold.start)) {
this.expand(fold);
} else {
this.collapse(fold);
}
}
/* 收起指定区域 */
collapse(fold) {
this.collapsedLines.add(fold.start);
this.updateGutter();
this.emitChange('collapsed', fold);
}
/* 展开指定区域 */
expand(fold) {
this.collapsedLines.delete(fold.start);
this.updateGutter();
this.emitChange('expanded', fold);
}
/* 折叠所有可折叠区域 */
collapseAll() {
this.foldRanges.forEach(fold => {
this.collapsedLines.add(fold.start);
});
this.updateGutter();
}
/* 展开所有区域 */
expandAll() {
this.collapsedLines.clear();
this.updateGutter();
}
}
折叠层级显示
在折叠区域显示摘要信息,如函数签名或前几行代码,帮助用户了解被折叠内容的主题。
折叠 glyph 渲染
在行号栏显示折叠图标:
/* 折叠图标渲染 */
class FoldGutterRenderer {
constructor(gutter) {
this.gutter = gutter;
}
/* 渲染折叠图标 */
render(fold, isCollapsed) {
const icon = document.createElement('span');
icon.className = `fold-icon ${isCollapsed ? 'collapsed' : 'expanded'}`;
/* 使用字符或 SVG */
icon.textContent = isCollapsed ? '▶' : '▼';
/* 悬停显示预览 */
icon.title = getFoldPreview(fold);
icon.onclick = () => toggleFold(fold.start);
return icon;
}
/* 获取折叠区域预览文本 */
getPreviewText(fold, lines) {
const firstLine = lines[fold.start];
const lastLine = lines[fold.end - 1];
/* 截取有意义的部分作为预览 */
return `${firstLine.trim()} ... ${lastLine.trim()}`;
}
}
折叠与滚动联动
确保折叠后的视图行为正确:
/* 滚动时自动展开父区域 */
class FoldScrollManager {
constructor(editor, foldManager) {
this.editor = editor;
this.foldManager = foldManager;
}
/* 滚动时确保当前行可见 */
onScroll(scrollTop) {
const visibleLine = this.getVisibleLineAt(scrollTop);
/* 检查当前行是否在折叠区域内 */
const parentFold = this.foldManager.getEnclosingFold(visibleLine);
if (parentFold && this.foldManager.isCollapsed(parentFold.start)) {
/* 自动展开 */
this.foldManager.expand(parentFold);
}
}
/* 跳转到指定行时收起其他区域 */
jumpToLine(lineNumber) {
const targetFold = this.foldManager.getFoldAtLine(lineNumber);
const visibleFolds = this.foldManager.getVisibleFolds();
/* 收起不在当前可视区域的折叠 */
visibleFolds.forEach(fold => {
if (fold !== targetFold) {
this.foldManager.collapse(fold);
}
});
}
}
快捷键支持
提供便捷的键盘操作:
/* 折叠快捷键配置 */
const FOLD_SHORTCUTS = {
'Ctrl+Shift+[': foldCurrentRegion, // 折叠当前区域
'Ctrl+Shift+]': unfoldCurrentRegion, // 展开当前区域
'Ctrl+K Ctrl+[': foldAll, // 折叠所有
'Ctrl+K Ctrl+]': unfoldAll, // 展开所有
'Ctrl+K 0': foldAll, // 折叠所有(数字 0)
'Ctrl+K J': unfoldAll, // 展开所有(Unfold)*/
};
function registerFoldShortcuts(editor) {
for (const [key, action] of Object.entries(FOLD_SHORTCUTS)) {
editor.addCommand(key, () => action(editor));
}
}
总结
- 缩进折叠:基于代码缩进级别,通用但精度较低
- 语法折叠:根据语言语法结构,精确识别函数、类等区域
- 折叠状态管理:维护展开/收起状态,支持持久化
- glyph 显示:在行号栏显示折叠图标和摘要
- 快捷键支持:提供常用操作的键盘快捷方式