/** * LaTeX Folding Range Provider for Monaco Editor. * Provides code folding for LaTeX environments and sections. */ import type * as Monaco from 'monaco-editor'; export class LaTeXFoldingRangeProvider implements Monaco.languages.FoldingRangeProvider { constructor(private monaco: typeof Monaco) {} provideFoldingRanges( model: Monaco.editor.ITextModel, context: Monaco.languages.FoldingContext, token: Monaco.CancellationToken ): Monaco.languages.ProviderResult { const ranges: Monaco.languages.FoldingRange[] = []; const lineCount = model.getLineCount(); // Stack for tracking nested environments const envStack: { name: string; line: number }[] = []; // Stack for sections (hierarchical) const sectionStack: { level: number; line: number }[] = []; const sectionLevels: Record = { part: 0, chapter: 1, section: 2, subsection: 3, subsubsection: 4, paragraph: 5, subparagraph: 6, }; // Multi-line comment tracking let commentStart: number | null = null; for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) { const line = model.getLineContent(lineNumber); // Check for \begin{environment} const beginMatch = line.match(/\\begin\{(\w+\*?)\}/); if (beginMatch) { envStack.push({ name: beginMatch[1], line: lineNumber }); } // Check for \end{environment} const endMatch = line.match(/\\end\{(\w+\*?)\}/); if (endMatch && envStack.length > 0) { // Find matching begin for (let i = envStack.length - 1; i >= 0; i--) { if (envStack[i].name === endMatch[1]) { if (lineNumber > envStack[i].line) { ranges.push({ start: envStack[i].line, end: lineNumber, kind: this.monaco.languages.FoldingRangeKind.Region, }); } envStack.splice(i, 1); break; } } } // Check for sections const sectionMatch = line.match( /\\(part|chapter|section|subsection|subsubsection|paragraph|subparagraph)\*?\s*[\[{]/ ); if (sectionMatch) { const level = sectionLevels[sectionMatch[1]]; // Close any sections at same or higher level (lower number = higher level) while ( sectionStack.length > 0 && sectionStack[sectionStack.length - 1].level >= level ) { const prev = sectionStack.pop()!; if (lineNumber - prev.line > 1) { ranges.push({ start: prev.line, end: lineNumber - 1, kind: this.monaco.languages.FoldingRangeKind.Region, }); } } sectionStack.push({ level, line: lineNumber }); } // Check for comment blocks (consecutive comment lines) const isComment = line.trim().startsWith('%'); if (isComment && commentStart === null) { commentStart = lineNumber; } else if (!isComment && commentStart !== null) { if (lineNumber - commentStart > 2) { ranges.push({ start: commentStart, end: lineNumber - 1, kind: this.monaco.languages.FoldingRangeKind.Comment, }); } commentStart = null; } } // Close remaining sections at end of document while (sectionStack.length > 0) { const prev = sectionStack.pop()!; if (lineCount - prev.line > 1) { ranges.push({ start: prev.line, end: lineCount, kind: this.monaco.languages.FoldingRangeKind.Region, }); } } // Close any unclosed comment block if (commentStart !== null && lineCount - commentStart > 2) { ranges.push({ start: commentStart, end: lineCount, kind: this.monaco.languages.FoldingRangeKind.Comment, }); } return ranges; } } /** * Creates a folding range provider for LaTeX. */ export function createFoldingProvider(monaco: typeof Monaco): LaTeXFoldingRangeProvider { return new LaTeXFoldingRangeProvider(monaco); }