Skip to content

Commit e990198

Browse files
authored
feat: monaco editor folding range provider (#11)
1 parent 0310c8a commit e990198

File tree

5 files changed

+114
-8
lines changed

5 files changed

+114
-8
lines changed

README.md

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ npm install @nuxtlabs/monarch-mdc
1919

2020
## Usage
2121

22-
The package provides both the MDC language syntax for the Monaco Editor, along with an optional formatter function.
22+
The package provides both the MDC language syntax for the Monaco Editor, along with optional formatting and code folding providers.
2323

2424
Checkout the [Editor.vue](./playground/components/Editor.vue) for a complete example.
2525

@@ -82,7 +82,7 @@ monaco.languages.registerDocumentFormattingEditProvider('mdc', {
8282
tabSize: TAB_SIZE,
8383
}),
8484
}],
85-
});
85+
})
8686

8787
// Register format on type provider
8888
monaco.languages.registerOnTypeFormattingEditProvider('mdc', {
@@ -96,7 +96,7 @@ monaco.languages.registerOnTypeFormattingEditProvider('mdc', {
9696
isFormatOnType: true,
9797
}),
9898
}],
99-
});
99+
})
100100

101101
const code = `
102102
Your **awesome** markdown
@@ -121,6 +121,43 @@ const editor = monaco.editor.create(el, {
121121
})
122122
```
123123

124+
### Code Folding
125+
126+
If you'd like to enable code folding for MDC block components into your Monaco Editor instance, you can also register the folding range provider.
127+
128+
```js
129+
import * as monaco from 'monaco-editor'
130+
import { language as markdownLanguage, foldingProvider as markdownFoldingProvider } from '@nuxtlabs/monarch-mdc'
131+
132+
// Register language
133+
monaco.languages.register({ id: 'mdc' })
134+
monaco.languages.setMonarchTokensProvider('mdc', markdownLanguage)
135+
136+
// Register code folding provider
137+
monaco.languages.registerFoldingRangeProvider('mdc', {
138+
provideFoldingRanges: model => markdownFoldingProvider(model),
139+
})
140+
141+
const code = `
142+
Your **awesome** markdown
143+
`
144+
145+
// Create monaco model
146+
const model = monaco.editor.createModel(
147+
code,
148+
'mdc'
149+
)
150+
151+
// Create your editor
152+
const el = ... // DOM element
153+
const editor = monaco.editor.create(el, {
154+
model,
155+
folding: true, // Enable code folding for MDC block components
156+
// Other Monaco Editor options
157+
// see: https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html
158+
})
159+
```
160+
124161
## 💻 Development
125162

126163
- Clone repository

playground/components/Editor.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
<script setup>
99
import { ref, onMounted, watch } from 'vue'
1010
import loader from '@monaco-editor/loader'
11-
import { language as mdc, formatter as mdcFormatter } from '../../src/index'
11+
import {
12+
language as mdc,
13+
formatter as mdcFormatter,
14+
foldingProvider as mdcFoldingProvider,
15+
} from '../../src/index'
1216
1317
const props = defineProps({
1418
code: {
@@ -48,6 +52,7 @@ onMounted(async () => {
4852
}),
4953
}],
5054
})
55+
5156
// Register format on type provider
5257
monaco.languages.registerOnTypeFormattingEditProvider('mdc', {
5358
// Auto-format when the user types a newline character.
@@ -62,6 +67,11 @@ onMounted(async () => {
6267
}],
6368
})
6469
70+
// Register code folding provider
71+
monaco.languages.registerFoldingRangeProvider('mdc', {
72+
provideFoldingRanges: model => mdcFoldingProvider(model),
73+
})
74+
6575
editor = monaco.editor.create(editorContainer.value, {
6676
value: props.code,
6777
language: props.language,
@@ -81,6 +91,7 @@ onMounted(async () => {
8191
bracketPairColorization: {
8292
enabled: true,
8393
},
94+
folding: true, // Enable code folding for MDC block components
8495
tabSize: TAB_SIZE, // Utilize the same tabSize used in the format providers
8596
detectIndentation: false, // Important as to not override tabSize
8697
insertSpaces: true, // Since the formatter utilizes spaces, we set to true to insert spaces when pressing Tab

src/folding-provider.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { languages, editor } from 'monaco-editor-core'
2+
3+
/**
4+
* Provides folding ranges for the MDC language in the Monaco editor.
5+
*
6+
* @param {monaco.editor.ITextModel} model - The text model to provide folding ranges for.
7+
* @returns {languages.ProviderResult<languages.FoldingRange[]>} An array of folding ranges for the editor.
8+
*
9+
* The function identifies folding ranges based on:
10+
* - Custom block components defined by start tags (e.g., "::container" or ":::button")
11+
* and end tags (e.g., "::" or ":::" with matching opening tag level).
12+
* - Markdown code blocks delimited by triple backticks (```) or tildes (~~~).
13+
*/
14+
export const foldingProvider = (model: editor.ITextModel): languages.ProviderResult<languages.FoldingRange[]> => {
15+
const ranges = [] // Array to store folding ranges
16+
const stack = [] // Stack to manage nested block components
17+
const lines = model.getLinesContent() // Retrieve all lines
18+
let insideCodeBlock = false // Flag to track if inside a code block
19+
20+
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
21+
const line = lines[lineNumber].trim() // Remove extra whitespace
22+
23+
// Check if the current line starts or ends a markdown code block
24+
if (/^\s*(?:`{3,}|~{3,})/.test(line)) {
25+
insideCodeBlock = !insideCodeBlock // Toggle code block mode
26+
continue // Skip further processing for this line
27+
}
28+
29+
// Skip processing lines inside a markdown code block
30+
if (insideCodeBlock) {
31+
continue
32+
}
33+
34+
// Match the start tag (e.g., "::container" or ":::button")
35+
const startMatch = line.match(/^\s*:{2,}([\w-]+)/)
36+
if (startMatch) {
37+
// Push start block onto the stack
38+
stack.push({ start: lineNumber + 1, tagName: startMatch[1] }) // Save 1-based line number and tag name
39+
continue // Skip further processing for this line
40+
}
41+
42+
// Match the end tag (e.g., "::" or ":::" with matching opening tag level)
43+
const endMatch = line.match(/^\s*:{2,}$/)
44+
if (endMatch && stack.length > 0) {
45+
const lastBlock = stack.pop() // Retrieve the last unmatched start block
46+
ranges.push({
47+
start: lastBlock?.start ?? 0, // Block start line (1-based)
48+
end: lineNumber + 1, // Current line as block end (1-based)
49+
})
50+
}
51+
}
52+
53+
// Return all folding ranges to the editor
54+
return ranges
55+
}

src/formatter.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const COMPONENT_START_REGEX = /^\s*:{2,}[\w-]+/
2929
const COMPONENT_END_REGEX = /^\s*:{2,}\s*$/
3030
// Matches YAML multiline indicators "|" or ">"
3131
const MULTILINE_STRING_REGEX = /^[\w-]+:\s*[|>]/
32+
// Matches markdown code block opening tags like "```" or "~~~"
33+
const CODE_BLOCK_REGEX = /^\s*(?:`{3,}|~{3,})/
3234

3335
/**
3436
* Cache for commonly used indentation strings to avoid repeated string creation
@@ -71,9 +73,9 @@ export const formatter = (content: string, { tabSize = 2, isFormatOnType = false
7173
let insideCodeBlock = false
7274
// Current position in output array
7375
let formattedIndex = 0
74-
// Base indent for the current markdown code block
76+
77+
// Add new state variable at top of function
7578
let codeBlockBaseIndent: number | null = null
76-
// The original indent of the markdown code block line
7779
let codeBlockOriginalIndent: number | null = null
7880

7981
const yamlState: YamlState = {
@@ -101,8 +103,8 @@ export const formatter = (content: string, { tabSize = 2, isFormatOnType = false
101103
continue
102104
}
103105

104-
// Handle code block markers (```)
105-
if (trimmedContent.startsWith('```')) {
106+
// Handle code block markers (``` or ~~~)
107+
if (CODE_BLOCK_REGEX.test(trimmedContent)) {
106108
insideCodeBlock = !insideCodeBlock
107109
if (insideCodeBlock) {
108110
codeBlockBaseIndent = parentIndent

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,4 @@ export const language = <languages.IMonarchLanguage>{
253253
}
254254

255255
export { formatter } from './formatter'
256+
export { foldingProvider } from './folding-provider'

0 commit comments

Comments
 (0)