/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import { TokenType, Scanner } from '../parser/cssScanner'; import { SCSSScanner, InterpolationFunction } from '../parser/scssScanner'; import { LESSScanner } from '../parser/lessScanner'; export function getFoldingRanges(document, context) { const ranges = computeFoldingRanges(document); return limitFoldingRanges(ranges, context); } function computeFoldingRanges(document) { function getStartLine(t) { return document.positionAt(t.offset).line; } function getEndLine(t) { return document.positionAt(t.offset + t.len).line; } function getScanner() { switch (document.languageId) { case 'scss': return new SCSSScanner(); case 'less': return new LESSScanner(); default: return new Scanner(); } } function tokenToRange(t, kind) { const startLine = getStartLine(t); const endLine = getEndLine(t); if (startLine !== endLine) { return { startLine, endLine, kind }; } else { return null; } } const ranges = []; const delimiterStack = []; const scanner = getScanner(); scanner.ignoreComment = false; scanner.setSource(document.getText()); let token = scanner.scan(); let prevToken = null; while (token.type !== TokenType.EOF) { switch (token.type) { case TokenType.CurlyL: case InterpolationFunction: { delimiterStack.push({ line: getStartLine(token), type: 'brace', isStart: true }); break; } case TokenType.CurlyR: { if (delimiterStack.length !== 0) { const prevDelimiter = popPrevStartDelimiterOfType(delimiterStack, 'brace'); if (!prevDelimiter) { break; } let endLine = getEndLine(token); if (prevDelimiter.type === 'brace') { /** * Other than the case when curly brace is not on a new line by itself, for example * .foo { * color: red; } * Use endLine minus one to show ending curly brace */ if (prevToken && getEndLine(prevToken) !== endLine) { endLine--; } if (prevDelimiter.line !== endLine) { ranges.push({ startLine: prevDelimiter.line, endLine, kind: undefined }); } } } break; } /** * In CSS, there is no single line comment prefixed with // * All comments are marked as `Comment` */ case TokenType.Comment: { const commentRegionMarkerToDelimiter = (marker) => { if (marker === '#region') { return { line: getStartLine(token), type: 'comment', isStart: true }; } else { return { line: getEndLine(token), type: 'comment', isStart: false }; } }; const getCurrDelimiter = (token) => { const matches = token.text.match(/^\s*\/\*\s*(#region|#endregion)\b\s*(.*?)\s*\*\//); if (matches) { return commentRegionMarkerToDelimiter(matches[1]); } else if (document.languageId === 'scss' || document.languageId === 'less') { const matches = token.text.match(/^\s*\/\/\s*(#region|#endregion)\b\s*(.*?)\s*/); if (matches) { return commentRegionMarkerToDelimiter(matches[1]); } } return null; }; const currDelimiter = getCurrDelimiter(token); // /* */ comment region folding // All #region and #endregion cases if (currDelimiter) { if (currDelimiter.isStart) { delimiterStack.push(currDelimiter); } else { const prevDelimiter = popPrevStartDelimiterOfType(delimiterStack, 'comment'); if (!prevDelimiter) { break; } if (prevDelimiter.type === 'comment') { if (prevDelimiter.line !== currDelimiter.line) { ranges.push({ startLine: prevDelimiter.line, endLine: currDelimiter.line, kind: 'region' }); } } } } // Multiline comment case else { const range = tokenToRange(token, 'comment'); if (range) { ranges.push(range); } } break; } } prevToken = token; token = scanner.scan(); } return ranges; } function popPrevStartDelimiterOfType(stack, type) { if (stack.length === 0) { return null; } for (let i = stack.length - 1; i >= 0; i--) { if (stack[i].type === type && stack[i].isStart) { return stack.splice(i, 1)[0]; } } return null; } /** * - Sort regions * - Remove invalid regions (intersections) * - If limit exceeds, only return `rangeLimit` amount of ranges */ function limitFoldingRanges(ranges, context) { const maxRanges = context && context.rangeLimit || Number.MAX_VALUE; const sortedRanges = ranges.sort((r1, r2) => { let diff = r1.startLine - r2.startLine; if (diff === 0) { diff = r1.endLine - r2.endLine; } return diff; }); const validRanges = []; let prevEndLine = -1; sortedRanges.forEach(r => { if (!(r.startLine < prevEndLine && prevEndLine < r.endLine)) { validRanges.push(r); prevEndLine = r.endLine; } }); if (validRanges.length < maxRanges) { return validRanges; } else { return validRanges.slice(0, maxRanges); } }