Monaco Editor in Action: Adding Code Folding

ohdarling
4 min readSep 11, 2023

--

In the previous article 4 Steps to Add Custom Language Support to Monaco Editor, we learned how to use Monaco Editor to create an editor that supports custom programming languages. However, as a standard code editor, we also need it to support code folding. This allows us to conveniently collapse certain code blocks when working with code files that have a large amount of content, making it easier to understand the overall code structure while reading the code.

In this article, we will explore how to add code folding functionality to Monaco Editor.

Official Example

In the Folding Provider Example of the Monaco Editor Playground, you can see how to add code folding support to a custom language:

monaco.languages.registerFoldingRangeProvider("foldLanguage", {
provideFoldingRanges: function (model, context, token) {
return [
// comment1
{
start: 5,
end: 7,
kind: monaco.languages.FoldingRangeKind.Comment,
},
// ...
]
}
});

Indeed, as you can see, after registering a custom language, you simply need to register a foldingRangeProvider. In the provideFoldingRanges callback function, you can identify the code blocks that can be folded, define the range by specifying the start and end line numbers, and return them to Monaco Editor.

In the example, the folding range was provided by hard-coding the start and end line numbers. However, in real-world usage, it’s not feasible to anticipate all the collapsible ranges in advance. Instead, we need to dynamically generate the folding ranges using a coding approach.

Requirement Definition

When developing the code editor for MermaidEditor for macOS & iOS, there are hierarchical relationships between code blocks in some charts. In order to provide a better editing experience, we need to add code block folding functionality to the code editor.

Currently, Monaco Editor does not provide official support for the Language Server Protocol (LSP). In order to implement code folding logic, we would need to explore alternative approaches.

Indeed, by observing the code, we can notice that code blocks at different levels often have different levels of indentation. Therefore, we can use the indentation to determine whether code lines belong to the same code block.

Code Implementation

Get Editor Content

In the provideFoldingRanges function, there is a parameter called model. Through this variable, we can obtain all the text content in the current editor area. By iterating through each line of content, we can get the number of preceding whitespace characters in each line. This allows us to determine the code block folding range based on the number of whitespace characters.

// Regular expression to match preceding whitespace characters in each line
const pattern = /^(\s*)(.+)/;
// Iterating through each line of code in the editor
for (var i = 1, count = model.getLineCount(); i <= count; i++) {
const line = model.getLineContent(i);

// Getting preceding whitespace characters in each line of code
const matches = pattern.exec(line);
if (matches) {
// Getting the length of whitespace in the current line to match subsequent lines of code belonging to the same block
const indentLen = matches[1].length;
}
}

Matching Code Folding Regions

After iterating through each line of code, we need to determine if the subsequent lines belong to a foldable code block. The criterion for this determination is that the preceding whitespace length of the subsequent lines is greater than the preceding whitespace length of the current line. Therefore, any code region with an indentation length greater than the current line can be included.

// Searching from the next line
let endLine = i + 1;
let lastNotEmptyLine = i;
// Iterating until the end of the editor area content
while (endLine <= count) {
const lineContent = model.getLineContent(endLine);
// Getting the preceding whitespace length of subsequent lines
const subMatches = pattern.exec(lineContent);
if (subMatches != null) {
// If the preceding whitespace length is greater than that of the starting line, include it in the folding region.
if (subMatches[1].length > indentLen) {
lastNotEmptyLine = endLine;
} else {
break;
}
}
endLine++;
}

Please note that blank lines are additionally ignored here to prevent them from interrupting the folding region determination. By ignoring blank lines, it ensures that the folding region determination is not affected by whitespace added for code formatting purposes within code blocks with the same indentation level. This allows for an accurate determination of the folding region and avoids prematurely ending the folding region determination.

Returning Code Block Folding Regions

if (lastNotEmptyLine > i) {
ranges.push({
start: i,
end: lastNotEmptyLine,
kind: monaco.languages.FoldingRangeKind.Region,
});
}

Here is the data structure for code block folding regions that can be used with Monaco Editor.

Complete Code

monacoEditor.languages.registerFoldingRangeProvider("foldLanguage", {
provideFoldingRanges: function (model, context, token) {
const ranges = [];
const pattern = /^(\s*)(.+)/;
for (var i = 1, count = model.getLineCount(); i <= count; i++) {
const line = model.getLineContent(i);

const matches = pattern.exec(line);
if (matches) {
const indentLen = matches[1].length;

let endLine = i + 1;
let lastNotEmptyLine = i;
while (endLine <= count) {
const lineContent = model.getLineContent(endLine);
const subMatches = pattern.exec(lineContent);
if (subMatches != null) {
if (subMatches[1].length > indentLen) {
lastNotEmptyLine = endLine;
} else {
break;
}
}
endLine++;
}

if (lastNotEmptyLine > i) {
ranges.push({
start: i,
end: lastNotEmptyLine,
kind: monaco.languages.FoldingRangeKind.Region,
});
}
}
}
return ranges;
},
});

Final Result

As seen, the final Monaco Editor correctly displays code block folding indicators, including nested folding, and is able to recognize them accurately.

References

--

--