monaco-editor-core
Version:
A browser based code editor
403 lines (397 loc) • 20.3 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { findLast } from '../../../base/common/arraysFind.js';
import * as strings from '../../../base/common/strings.js';
import { CursorColumns } from '../core/cursorColumns.js';
import { Range } from '../core/range.js';
import { TextModelPart } from './textModelPart.js';
import { computeIndentLevel } from './utils.js';
import { HorizontalGuidesState, IndentGuide, IndentGuideHorizontalLine } from '../textModelGuides.js';
import { BugIndicatingError } from '../../../base/common/errors.js';
export class GuidesTextModelPart extends TextModelPart {
constructor(textModel, languageConfigurationService) {
super();
this.textModel = textModel;
this.languageConfigurationService = languageConfigurationService;
}
getLanguageConfiguration(languageId) {
return this.languageConfigurationService.getLanguageConfiguration(languageId);
}
_computeIndentLevel(lineIndex) {
return computeIndentLevel(this.textModel.getLineContent(lineIndex + 1), this.textModel.getOptions().tabSize);
}
getActiveIndentGuide(lineNumber, minLineNumber, maxLineNumber) {
this.assertNotDisposed();
const lineCount = this.textModel.getLineCount();
if (lineNumber < 1 || lineNumber > lineCount) {
throw new BugIndicatingError('Illegal value for lineNumber');
}
const foldingRules = this.getLanguageConfiguration(this.textModel.getLanguageId()).foldingRules;
const offSide = Boolean(foldingRules && foldingRules.offSide);
let up_aboveContentLineIndex = -2; /* -2 is a marker for not having computed it */
let up_aboveContentLineIndent = -1;
let up_belowContentLineIndex = -2; /* -2 is a marker for not having computed it */
let up_belowContentLineIndent = -1;
const up_resolveIndents = (lineNumber) => {
if (up_aboveContentLineIndex !== -1 &&
(up_aboveContentLineIndex === -2 ||
up_aboveContentLineIndex > lineNumber - 1)) {
up_aboveContentLineIndex = -1;
up_aboveContentLineIndent = -1;
// must find previous line with content
for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {
const indent = this._computeIndentLevel(lineIndex);
if (indent >= 0) {
up_aboveContentLineIndex = lineIndex;
up_aboveContentLineIndent = indent;
break;
}
}
}
if (up_belowContentLineIndex === -2) {
up_belowContentLineIndex = -1;
up_belowContentLineIndent = -1;
// must find next line with content
for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {
const indent = this._computeIndentLevel(lineIndex);
if (indent >= 0) {
up_belowContentLineIndex = lineIndex;
up_belowContentLineIndent = indent;
break;
}
}
}
};
let down_aboveContentLineIndex = -2; /* -2 is a marker for not having computed it */
let down_aboveContentLineIndent = -1;
let down_belowContentLineIndex = -2; /* -2 is a marker for not having computed it */
let down_belowContentLineIndent = -1;
const down_resolveIndents = (lineNumber) => {
if (down_aboveContentLineIndex === -2) {
down_aboveContentLineIndex = -1;
down_aboveContentLineIndent = -1;
// must find previous line with content
for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {
const indent = this._computeIndentLevel(lineIndex);
if (indent >= 0) {
down_aboveContentLineIndex = lineIndex;
down_aboveContentLineIndent = indent;
break;
}
}
}
if (down_belowContentLineIndex !== -1 &&
(down_belowContentLineIndex === -2 ||
down_belowContentLineIndex < lineNumber - 1)) {
down_belowContentLineIndex = -1;
down_belowContentLineIndent = -1;
// must find next line with content
for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {
const indent = this._computeIndentLevel(lineIndex);
if (indent >= 0) {
down_belowContentLineIndex = lineIndex;
down_belowContentLineIndent = indent;
break;
}
}
}
};
let startLineNumber = 0;
let goUp = true;
let endLineNumber = 0;
let goDown = true;
let indent = 0;
let initialIndent = 0;
for (let distance = 0; goUp || goDown; distance++) {
const upLineNumber = lineNumber - distance;
const downLineNumber = lineNumber + distance;
if (distance > 1 && (upLineNumber < 1 || upLineNumber < minLineNumber)) {
goUp = false;
}
if (distance > 1 &&
(downLineNumber > lineCount || downLineNumber > maxLineNumber)) {
goDown = false;
}
if (distance > 50000) {
// stop processing
goUp = false;
goDown = false;
}
let upLineIndentLevel = -1;
if (goUp && upLineNumber >= 1) {
// compute indent level going up
const currentIndent = this._computeIndentLevel(upLineNumber - 1);
if (currentIndent >= 0) {
// This line has content (besides whitespace)
// Use the line's indent
up_belowContentLineIndex = upLineNumber - 1;
up_belowContentLineIndent = currentIndent;
upLineIndentLevel = Math.ceil(currentIndent / this.textModel.getOptions().indentSize);
}
else {
up_resolveIndents(upLineNumber);
upLineIndentLevel = this._getIndentLevelForWhitespaceLine(offSide, up_aboveContentLineIndent, up_belowContentLineIndent);
}
}
let downLineIndentLevel = -1;
if (goDown && downLineNumber <= lineCount) {
// compute indent level going down
const currentIndent = this._computeIndentLevel(downLineNumber - 1);
if (currentIndent >= 0) {
// This line has content (besides whitespace)
// Use the line's indent
down_aboveContentLineIndex = downLineNumber - 1;
down_aboveContentLineIndent = currentIndent;
downLineIndentLevel = Math.ceil(currentIndent / this.textModel.getOptions().indentSize);
}
else {
down_resolveIndents(downLineNumber);
downLineIndentLevel = this._getIndentLevelForWhitespaceLine(offSide, down_aboveContentLineIndent, down_belowContentLineIndent);
}
}
if (distance === 0) {
initialIndent = upLineIndentLevel;
continue;
}
if (distance === 1) {
if (downLineNumber <= lineCount &&
downLineIndentLevel >= 0 &&
initialIndent + 1 === downLineIndentLevel) {
// This is the beginning of a scope, we have special handling here, since we want the
// child scope indent to be active, not the parent scope
goUp = false;
startLineNumber = downLineNumber;
endLineNumber = downLineNumber;
indent = downLineIndentLevel;
continue;
}
if (upLineNumber >= 1 &&
upLineIndentLevel >= 0 &&
upLineIndentLevel - 1 === initialIndent) {
// This is the end of a scope, just like above
goDown = false;
startLineNumber = upLineNumber;
endLineNumber = upLineNumber;
indent = upLineIndentLevel;
continue;
}
startLineNumber = lineNumber;
endLineNumber = lineNumber;
indent = initialIndent;
if (indent === 0) {
// No need to continue
return { startLineNumber, endLineNumber, indent };
}
}
if (goUp) {
if (upLineIndentLevel >= indent) {
startLineNumber = upLineNumber;
}
else {
goUp = false;
}
}
if (goDown) {
if (downLineIndentLevel >= indent) {
endLineNumber = downLineNumber;
}
else {
goDown = false;
}
}
}
return { startLineNumber, endLineNumber, indent };
}
getLinesBracketGuides(startLineNumber, endLineNumber, activePosition, options) {
const result = [];
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
result.push([]);
}
// If requested, this could be made configurable.
const includeSingleLinePairs = true;
const bracketPairs = this.textModel.bracketPairs.getBracketPairsInRangeWithMinIndentation(new Range(startLineNumber, 1, endLineNumber, this.textModel.getLineMaxColumn(endLineNumber))).toArray();
let activeBracketPairRange = undefined;
if (activePosition && bracketPairs.length > 0) {
const bracketsContainingActivePosition = (startLineNumber <= activePosition.lineNumber &&
activePosition.lineNumber <= endLineNumber
// We don't need to query the brackets again if the cursor is in the viewport
? bracketPairs
: this.textModel.bracketPairs.getBracketPairsInRange(Range.fromPositions(activePosition)).toArray()).filter((bp) => Range.strictContainsPosition(bp.range, activePosition));
activeBracketPairRange = findLast(bracketsContainingActivePosition, (i) => includeSingleLinePairs || i.range.startLineNumber !== i.range.endLineNumber)?.range;
}
const independentColorPoolPerBracketType = this.textModel.getOptions().bracketPairColorizationOptions.independentColorPoolPerBracketType;
const colorProvider = new BracketPairGuidesClassNames();
for (const pair of bracketPairs) {
/*
{
|
}
{
|
----}
____{
|test
----}
renderHorizontalEndLineAtTheBottom:
{
|
|x}
--
renderHorizontalEndLineAtTheBottom:
____{
|test
| x }
----
*/
if (!pair.closingBracketRange) {
continue;
}
const isActive = activeBracketPairRange && pair.range.equalsRange(activeBracketPairRange);
if (!isActive && !options.includeInactive) {
continue;
}
const className = colorProvider.getInlineClassName(pair.nestingLevel, pair.nestingLevelOfEqualBracketType, independentColorPoolPerBracketType) +
(options.highlightActive && isActive
? ' ' + colorProvider.activeClassName
: '');
const start = pair.openingBracketRange.getStartPosition();
const end = pair.closingBracketRange.getStartPosition();
const horizontalGuides = options.horizontalGuides === HorizontalGuidesState.Enabled || (options.horizontalGuides === HorizontalGuidesState.EnabledForActive && isActive);
if (pair.range.startLineNumber === pair.range.endLineNumber) {
if (includeSingleLinePairs && horizontalGuides) {
result[pair.range.startLineNumber - startLineNumber].push(new IndentGuide(-1, pair.openingBracketRange.getEndPosition().column, className, new IndentGuideHorizontalLine(false, end.column), -1, -1));
}
continue;
}
const endVisibleColumn = this.getVisibleColumnFromPosition(end);
const startVisibleColumn = this.getVisibleColumnFromPosition(pair.openingBracketRange.getStartPosition());
const guideVisibleColumn = Math.min(startVisibleColumn, endVisibleColumn, pair.minVisibleColumnIndentation + 1);
let renderHorizontalEndLineAtTheBottom = false;
const firstNonWsIndex = strings.firstNonWhitespaceIndex(this.textModel.getLineContent(pair.closingBracketRange.startLineNumber));
const hasTextBeforeClosingBracket = firstNonWsIndex < pair.closingBracketRange.startColumn - 1;
if (hasTextBeforeClosingBracket) {
renderHorizontalEndLineAtTheBottom = true;
}
const visibleGuideStartLineNumber = Math.max(start.lineNumber, startLineNumber);
const visibleGuideEndLineNumber = Math.min(end.lineNumber, endLineNumber);
const offset = renderHorizontalEndLineAtTheBottom ? 1 : 0;
for (let l = visibleGuideStartLineNumber; l < visibleGuideEndLineNumber + offset; l++) {
result[l - startLineNumber].push(new IndentGuide(guideVisibleColumn, -1, className, null, l === start.lineNumber ? start.column : -1, l === end.lineNumber ? end.column : -1));
}
if (horizontalGuides) {
if (start.lineNumber >= startLineNumber && startVisibleColumn > guideVisibleColumn) {
result[start.lineNumber - startLineNumber].push(new IndentGuide(guideVisibleColumn, -1, className, new IndentGuideHorizontalLine(false, start.column), -1, -1));
}
if (end.lineNumber <= endLineNumber && endVisibleColumn > guideVisibleColumn) {
result[end.lineNumber - startLineNumber].push(new IndentGuide(guideVisibleColumn, -1, className, new IndentGuideHorizontalLine(!renderHorizontalEndLineAtTheBottom, end.column), -1, -1));
}
}
}
for (const guides of result) {
guides.sort((a, b) => a.visibleColumn - b.visibleColumn);
}
return result;
}
getVisibleColumnFromPosition(position) {
return (CursorColumns.visibleColumnFromColumn(this.textModel.getLineContent(position.lineNumber), position.column, this.textModel.getOptions().tabSize) + 1);
}
getLinesIndentGuides(startLineNumber, endLineNumber) {
this.assertNotDisposed();
const lineCount = this.textModel.getLineCount();
if (startLineNumber < 1 || startLineNumber > lineCount) {
throw new Error('Illegal value for startLineNumber');
}
if (endLineNumber < 1 || endLineNumber > lineCount) {
throw new Error('Illegal value for endLineNumber');
}
const options = this.textModel.getOptions();
const foldingRules = this.getLanguageConfiguration(this.textModel.getLanguageId()).foldingRules;
const offSide = Boolean(foldingRules && foldingRules.offSide);
const result = new Array(endLineNumber - startLineNumber + 1);
let aboveContentLineIndex = -2; /* -2 is a marker for not having computed it */
let aboveContentLineIndent = -1;
let belowContentLineIndex = -2; /* -2 is a marker for not having computed it */
let belowContentLineIndent = -1;
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
const resultIndex = lineNumber - startLineNumber;
const currentIndent = this._computeIndentLevel(lineNumber - 1);
if (currentIndent >= 0) {
// This line has content (besides whitespace)
// Use the line's indent
aboveContentLineIndex = lineNumber - 1;
aboveContentLineIndent = currentIndent;
result[resultIndex] = Math.ceil(currentIndent / options.indentSize);
continue;
}
if (aboveContentLineIndex === -2) {
aboveContentLineIndex = -1;
aboveContentLineIndent = -1;
// must find previous line with content
for (let lineIndex = lineNumber - 2; lineIndex >= 0; lineIndex--) {
const indent = this._computeIndentLevel(lineIndex);
if (indent >= 0) {
aboveContentLineIndex = lineIndex;
aboveContentLineIndent = indent;
break;
}
}
}
if (belowContentLineIndex !== -1 &&
(belowContentLineIndex === -2 || belowContentLineIndex < lineNumber - 1)) {
belowContentLineIndex = -1;
belowContentLineIndent = -1;
// must find next line with content
for (let lineIndex = lineNumber; lineIndex < lineCount; lineIndex++) {
const indent = this._computeIndentLevel(lineIndex);
if (indent >= 0) {
belowContentLineIndex = lineIndex;
belowContentLineIndent = indent;
break;
}
}
}
result[resultIndex] = this._getIndentLevelForWhitespaceLine(offSide, aboveContentLineIndent, belowContentLineIndent);
}
return result;
}
_getIndentLevelForWhitespaceLine(offSide, aboveContentLineIndent, belowContentLineIndent) {
const options = this.textModel.getOptions();
if (aboveContentLineIndent === -1 || belowContentLineIndent === -1) {
// At the top or bottom of the file
return 0;
}
else if (aboveContentLineIndent < belowContentLineIndent) {
// we are inside the region above
return 1 + Math.floor(aboveContentLineIndent / options.indentSize);
}
else if (aboveContentLineIndent === belowContentLineIndent) {
// we are in between two regions
return Math.ceil(belowContentLineIndent / options.indentSize);
}
else {
if (offSide) {
// same level as region below
return Math.ceil(belowContentLineIndent / options.indentSize);
}
else {
// we are inside the region that ends below
return 1 + Math.floor(belowContentLineIndent / options.indentSize);
}
}
}
}
export class BracketPairGuidesClassNames {
constructor() {
this.activeClassName = 'indent-active';
}
getInlineClassName(nestingLevel, nestingLevelOfEqualBracketType, independentColorPoolPerBracketType) {
return this.getInlineClassNameOfLevel(independentColorPoolPerBracketType ? nestingLevelOfEqualBracketType : nestingLevel);
}
getInlineClassNameOfLevel(level) {
// To support a dynamic amount of colors up to 6 colors,
// we use a number that is a lcm of all numbers from 1 to 6.
return `bracket-indent-guide lvl-${level % 30}`;
}
}