代码编辑器代码折叠完全指南

代码折叠(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 显示:在行号栏显示折叠图标和摘要
  • 快捷键支持:提供常用操作的键盘快捷方式