190 lines
7 KiB
JavaScript
190 lines
7 KiB
JavaScript
/*---------------------------------------------------------------------------------------------
|
|
* 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);
|
|
}
|
|
}
|