提交 17d66171 编写于 作者: A Alex Dima

Add `editor.indentGuides` option, render indent guides

上级 52c47af7
......@@ -17,6 +17,7 @@ export class ViewLine implements IVisibleLineData {
protected _context:IViewContext;
private _renderWhitespace: boolean;
private _indentGuides: boolean;
private _spaceWidth: number;
private _lineHeight: number;
private _stopRenderingLineAfter: number;
......@@ -36,6 +37,7 @@ export class ViewLine implements IVisibleLineData {
constructor(context:IViewContext) {
this._context = context;
this._renderWhitespace = this._context.configuration.editor.renderWhitespace;
this._indentGuides = this._context.configuration.editor.indentGuides;
this._spaceWidth = this._context.configuration.editor.spaceWidth;
this._lineHeight = this._context.configuration.editor.lineHeight;
this._stopRenderingLineAfter = this._context.configuration.editor.stopRenderingLineAfter;
......@@ -83,6 +85,9 @@ export class ViewLine implements IVisibleLineData {
if (e.renderWhitespace) {
this._renderWhitespace = this._context.configuration.editor.renderWhitespace;
}
if (e.indentGuides) {
this._indentGuides = this._context.configuration.editor.indentGuides;
}
if (e.spaceWidth) {
this._spaceWidth = this._context.configuration.editor.spaceWidth;
}
......@@ -110,7 +115,8 @@ export class ViewLine implements IVisibleLineData {
this._context.model.getTabSize(),
this._context.model.getLineTokens(lineNumber),
inlineDecorations,
this._renderWhitespace
this._renderWhitespace,
this._indentGuides
);
}
......
......@@ -1768,7 +1768,7 @@ class InlineViewZonesComputer extends ViewZonesComputer {
let lineTokens = new editorCommon.ViewLineTokens([new editorCommon.ViewLineToken(0, '')], 0, lineContent.length);
let parts = createLineParts(lineNumber, 1, lineContent, tabSize, lineTokens, decorations, config.renderWhitespace);
let parts = createLineParts(lineNumber, 1, lineContent, tabSize, lineTokens, decorations, config.renderWhitespace, config.indentGuides);
let r = renderLine(new RenderLineInput(
lineContent,
......
......@@ -20,6 +20,10 @@
border-left: 1px solid lightgray;
box-sizing: border-box;
}
.monaco-editor.vs-dark .token.indent-guide,
.monaco-editor.hc-black .token.indent-guide {
border-left: 1px solid #353535;
}
.monaco-editor.enable-ligatures {
-webkit-font-feature-settings: "liga" on, "calt" on;
......
......@@ -107,6 +107,7 @@ export class InternalEditorOptions implements editorCommon.IInternalEditorOption
referenceInfos: boolean;
folding: boolean;
renderWhitespace: boolean;
indentGuides: boolean;
layoutInfo: editorCommon.IEditorLayoutInfo;
stylingInfo: editorCommon.IEditorStyling;
wrappingInfo: editorCommon.IEditorWrappingInfo;
......@@ -175,6 +176,7 @@ export class InternalEditorOptions implements editorCommon.IInternalEditorOption
this.referenceInfos = Boolean(input.referenceInfos);
this.folding = Boolean(input.folding);
this.renderWhitespace = Boolean(input.renderWhitespace);
this.indentGuides = Boolean(input.indentGuides);
this.layoutInfo = {
width: Number(input.layoutInfo.width)|0,
height: Number(input.layoutInfo.height)|0,
......@@ -365,6 +367,7 @@ class InternalEditorOptionsHelper {
referenceInfos: toBoolean(opts.referenceInfos),
folding: toBoolean(opts.folding),
renderWhitespace: toBoolean(opts.renderWhitespace),
indentGuides: toBoolean(opts.indentGuides),
layoutInfo: layoutInfo,
stylingInfo: {
......@@ -459,6 +462,7 @@ class InternalEditorOptionsHelper {
referenceInfos: (prevOpts.referenceInfos !== newOpts.referenceInfos),
folding: (prevOpts.folding !== newOpts.folding),
renderWhitespace: (prevOpts.renderWhitespace !== newOpts.renderWhitespace),
indentGuides: (prevOpts.indentGuides !== newOpts.indentGuides),
layoutInfo: (!EditorLayoutProvider.layoutEqual(prevOpts.layoutInfo, newOpts.layoutInfo)),
stylingInfo: (!this._stylingInfoEqual(prevOpts.stylingInfo, newOpts.stylingInfo)),
......@@ -932,6 +936,11 @@ let editorConfiguration:IConfigurationNode = {
default: DefaultConfig.editor.renderWhitespace,
description: nls.localize('renderWhitespace', "Controls whether the editor should render whitespace characters")
},
'editor.indentGuides': {
'type': 'boolean',
default: DefaultConfig.editor.indentGuides,
description: nls.localize('indentGuides', "Controls whether the editor should render indent guides")
},
'editor.referenceInfos' : {
'type': 'boolean',
'default': DefaultConfig.editor.referenceInfos,
......
......@@ -79,6 +79,7 @@ class ConfigClass implements IConfiguration {
referenceInfos: true,
folding: true,
renderWhitespace: false,
indentGuides: true,
fontFamily: '',
fontSize: 0,
......
......@@ -519,6 +519,11 @@ export interface IEditorOptions {
* Defaults to false.
*/
renderWhitespace?: boolean;
/**
* Enable rendering of indent guides.
* Defaults to true.
*/
indentGuides?: boolean;
/**
* The font family
*/
......@@ -643,6 +648,7 @@ export interface IInternalEditorOptions {
referenceInfos: boolean;
folding: boolean;
renderWhitespace: boolean;
indentGuides: boolean;
// ---- Options that are computed
......@@ -735,6 +741,7 @@ export interface IConfigurationChangedEvent {
referenceInfos: boolean;
folding: boolean;
renderWhitespace: boolean;
indentGuides: boolean;
// ---- Options that are computed
layoutInfo: boolean;
......
......@@ -9,16 +9,23 @@ import {Arrays} from 'vs/editor/common/core/arrays';
import {ViewLineToken, IEditorRange, ViewLineTokens} from 'vs/editor/common/editorCommon';
import {Range} from 'vs/editor/common/core/range';
const INDENT_GUIDES = false;
function cmpLineDecorations(a:ILineDecoration, b:ILineDecoration): number {
return Range.compareRangesUsingStarts(a.range, b.range);
}
export function createLineParts(lineNumber:number, minLineColumn:number, lineContent:string, tabSize:number, lineTokens:ViewLineTokens, rawLineDecorations:ILineDecoration[], renderWhitespace:boolean): LineParts {
if (INDENT_GUIDES || renderWhitespace) {
export function createLineParts(lineNumber:number, minLineColumn:number, lineContent:string, tabSize:number, lineTokens:ViewLineTokens, rawLineDecorations:ILineDecoration[], renderWhitespace:boolean, indentGuides:boolean): LineParts {
if (indentGuides || renderWhitespace) {
let customLineDecorations:CustomLineDecorations;
if (indentGuides && renderWhitespace) {
customLineDecorations = CustomLineDecorations.RenderWhitespaceAndIndentGuides;
} else if (renderWhitespace) {
customLineDecorations = CustomLineDecorations.RenderWhitespace;
} else {
customLineDecorations = CustomLineDecorations.RenderIndentGuides;
}
let oldLength = rawLineDecorations.length;
rawLineDecorations = insertWhitespace(INDENT_GUIDES, renderWhitespace, lineNumber, lineContent, tabSize, lineTokens.getFauxIndentLength(), rawLineDecorations);
rawLineDecorations = insertCustomLineDecorations(customLineDecorations, lineNumber, lineContent, tabSize, lineTokens.getFauxIndentLength(), rawLineDecorations);
if (rawLineDecorations.length !== oldLength) {
rawLineDecorations.sort(cmpLineDecorations);
}
......@@ -106,11 +113,21 @@ function insertOneWhitespace(dest:ILineDecoration[], lineNumber:number, startCol
});
}
function insertWhitespace(renderIndentGuides:boolean, renderWhitespace:boolean, lineNumber:number, lineContent: string, tabSize:number, fauxIndentLength: number, rawLineDecorations: ILineDecoration[]): ILineDecoration[] {
enum CustomLineDecorations {
RenderWhitespace,
RenderIndentGuides,
RenderWhitespaceAndIndentGuides
}
function insertCustomLineDecorations(customLineDecorations:CustomLineDecorations, lineNumber:number, lineContent: string, tabSize:number, fauxIndentLength: number, rawLineDecorations: ILineDecoration[]): ILineDecoration[] {
let lineLength = lineContent.length;
if (lineLength === fauxIndentLength) {
return rawLineDecorations;
}
if (fauxIndentLength > 0 && customLineDecorations === CustomLineDecorations.RenderIndentGuides) {
// no indent guides on faux indented lines
return rawLineDecorations;
}
let firstChar = lineContent.charCodeAt(fauxIndentLength);
let lastChar = lineContent.charCodeAt(lineLength - 1);
......@@ -134,17 +151,34 @@ function insertWhitespace(renderIndentGuides:boolean, renderWhitespace:boolean,
let tmpIndent = 0;
let whitespaceStartColumn = fauxIndentLength + 1;
let leadingClassName = '';
if (renderIndentGuides) {
leadingClassName += ' indent-guide';
}
if (renderWhitespace) {
leadingClassName += ' leading whitespace';
let leadingClassName:string;
switch (customLineDecorations) {
case CustomLineDecorations.RenderWhitespaceAndIndentGuides:
if (fauxIndentLength > 0) {
leadingClassName = 'leading whitespace';
} else {
leadingClassName = 'leading whitespace indent-guide';
}
break;
case CustomLineDecorations.RenderWhitespace:
leadingClassName = 'leading whitespace';
break;
case CustomLineDecorations.RenderIndentGuides:
leadingClassName = 'indent-guide';
break;
}
let trailingClassName = '';
if (renderWhitespace) {
trailingClassName += ' trailing whitespace';
let trailingClassName:string;
switch (customLineDecorations) {
case CustomLineDecorations.RenderWhitespaceAndIndentGuides:
trailingClassName = 'trailing whitespace';
break;
case CustomLineDecorations.RenderWhitespace:
trailingClassName = 'trailing whitespace';
break;
case CustomLineDecorations.RenderIndentGuides:
trailingClassName = '';
break;
}
for (let i = fauxIndentLength; i < lineLength; i++) {
......@@ -179,6 +213,12 @@ function insertWhitespace(renderIndentGuides:boolean, renderWhitespace:boolean,
}
if (i === lastNonWhitespaceIndex) {
if (customLineDecorations === CustomLineDecorations.RenderIndentGuides) {
// no need to create additional parts at the end when rendering indent guides
break;
}
whitespaceStartColumn = i + 2;
tmpIndent = tmpIndent % tabSize;
}
......
......@@ -80,9 +80,12 @@ export function renderLine(input:RenderLineInput): RenderLineOutput {
return renderLineActual(lineText, lineTextLength, tabSize, spaceWidth, actualLineParts.slice(0), renderWhitespace, charBreakIndex);
}
const WHITESPACE_TOKEN_TEST = /\bwhitespace\b/;
function isWhitespace(type:string): boolean {
return WHITESPACE_TOKEN_TEST.test(type);
return (type.indexOf('whitespace') >= 0);
}
function isIndentGuide(type:string): boolean {
return (type.indexOf('indent-guide') >= 0);
}
function renderLineActual(lineText:string, lineTextLength:number, tabSize:number, spaceWidth:number, actualLineParts:ViewLineToken[], renderWhitespace:boolean, charBreakIndex:number): RenderLineOutput {
......@@ -100,10 +103,8 @@ function renderLineActual(lineText:string, lineTextLength:number, tabSize:number
for (let partIndex = 0, partIndexLen = actualLineParts.length; partIndex < partIndexLen; partIndex++) {
let part = actualLineParts[partIndex];
let partRendersWhitespace = false;
if (renderWhitespace) {
partRendersWhitespace = isWhitespace(part.type);
}
let parsRendersWhitespace = (renderWhitespace && isWhitespace(part.type));
let partIsFixedWidth = parsRendersWhitespace || isIndentGuide(part.type);
let toCharIndex = lineTextLength;
if (partIndex + 1 < partIndexLen) {
......@@ -112,7 +113,7 @@ function renderLineActual(lineText:string, lineTextLength:number, tabSize:number
}
charOffsetInPart = 0;
if (partRendersWhitespace) {
if (partIsFixedWidth) {
let partContentCnt = 0;
let partContent = '';
......@@ -125,7 +126,7 @@ function renderLineActual(lineText:string, lineTextLength:number, tabSize:number
tabsCharDelta += insertSpacesCount - 1;
charOffsetInPart += insertSpacesCount - 1;
if (insertSpacesCount > 0) {
partContent += '&rarr;';
partContent += parsRendersWhitespace ? '&rarr;' : '&nbsp;';
partContentCnt++;
insertSpacesCount--;
}
......@@ -136,7 +137,7 @@ function renderLineActual(lineText:string, lineTextLength:number, tabSize:number
}
} else {
// must be _space
partContent += '&middot;';
partContent += parsRendersWhitespace ? '&middot;' : '&nbsp;';
partContentCnt++;
}
......@@ -171,10 +172,6 @@ function renderLineActual(lineText:string, lineTextLength:number, tabSize:number
let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize;
tabsCharDelta += insertSpacesCount - 1;
charOffsetInPart += insertSpacesCount - 1;
if (insertSpacesCount > 0) {
out += '&nbsp;';
insertSpacesCount--;
}
while (insertSpacesCount > 0) {
out += '&nbsp;';
insertSpacesCount--;
......
......@@ -5,9 +5,9 @@
'use strict';
import * as assert from 'assert';
import {DecorationSegment, ILineDecoration, LineDecorationsNormalizer, getColumnOfLinePartOffset} from 'vs/editor/common/viewLayout/viewLineParts';
import {DecorationSegment, ILineDecoration, LineDecorationsNormalizer, getColumnOfLinePartOffset, createLineParts} from 'vs/editor/common/viewLayout/viewLineParts';
import {Range} from 'vs/editor/common/core/range';
import {ViewLineToken} from 'vs/editor/common/editorCommon';
import {ViewLineToken, ViewLineTokens} from 'vs/editor/common/editorCommon';
import {RenderLineInput, renderLine} from 'vs/editor/common/viewLayout/viewLineRenderer';
suite('Editor ViewLayout - ViewLineParts', () => {
......@@ -59,6 +59,296 @@ suite('Editor ViewLayout - ViewLineParts', () => {
]);
});
function testCreateLineParts(lineContent: string, tokens: ViewLineToken[], fauxIndentLength: number, renderWhitespace:boolean, indentGuides:boolean, expected:ViewLineToken[]): void {
let lineParts = createLineParts(1, 1, lineContent, 4, new ViewLineTokens(tokens, fauxIndentLength, lineContent.length), [], renderWhitespace, indentGuides);
let actual = lineParts.getParts();
assert.deepEqual(actual, expected);
}
test('createLineParts simple', () => {
testCreateLineParts(
'Hello world!',
[
new ViewLineToken(0, '')
],
0,
false,
false,
[
new ViewLineToken(0, '')
]
);
});
test('createLineParts simple two tokens', () => {
testCreateLineParts(
'Hello world!',
[
new ViewLineToken(0, 'a'),
new ViewLineToken(6, 'b')
],
0,
false,
false,
[
new ViewLineToken(0, 'a'),
new ViewLineToken(6, 'b')
]
);
});
test('createLineParts render whitespace - 4 leading spaces', () => {
testCreateLineParts(
' Hello world! ',
[
new ViewLineToken(0, ''),
new ViewLineToken(4, 'a'),
new ViewLineToken(6, 'b')
],
0,
true,
false,
[
new ViewLineToken(0, ' leading whitespace'),
new ViewLineToken(4, 'a'),
new ViewLineToken(6, 'b'),
new ViewLineToken(16, 'b trailing whitespace')
]
);
});
test('createLineParts render whitespace - 8 leading spaces', () => {
testCreateLineParts(
' Hello world! ',
[
new ViewLineToken(0, ''),
new ViewLineToken(8, 'a'),
new ViewLineToken(10, 'b')
],
0,
true,
false,
[
new ViewLineToken(0, ' leading whitespace'),
new ViewLineToken(4, ' leading whitespace'),
new ViewLineToken(8, 'a'),
new ViewLineToken(10, 'b'),
new ViewLineToken(20, 'b trailing whitespace'),
new ViewLineToken(24, 'b trailing whitespace'),
]
);
});
test('createLineParts render whitespace - 2 leading tabs', () => {
testCreateLineParts(
'\t\tHello world!\t',
[
new ViewLineToken(0, ''),
new ViewLineToken(2, 'a'),
new ViewLineToken(4, 'b')
],
0,
true,
false,
[
new ViewLineToken(0, ' leading whitespace'),
new ViewLineToken(1, ' leading whitespace'),
new ViewLineToken(2, 'a'),
new ViewLineToken(4, 'b'),
new ViewLineToken(14, 'b trailing whitespace'),
]
);
});
test('createLineParts render whitespace - mixed leading spaces and tabs', () => {
testCreateLineParts(
' \t\t Hello world! \t \t \t ',
[
new ViewLineToken(0, ''),
new ViewLineToken(6, 'a'),
new ViewLineToken(8, 'b')
],
0,
true,
false,
[
new ViewLineToken(0, ' leading whitespace'),
new ViewLineToken(3, ' leading whitespace'),
new ViewLineToken(4, ' leading whitespace'),
new ViewLineToken(6, 'a'),
new ViewLineToken(8, 'b'),
new ViewLineToken(18, 'b trailing whitespace'),
new ViewLineToken(20, 'b trailing whitespace'),
new ViewLineToken(23, 'b trailing whitespace'),
new ViewLineToken(27, 'b trailing whitespace'),
]
);
});
test('createLineParts render indent guides - 4 leading spaces', () => {
testCreateLineParts(
' Hello world! ',
[
new ViewLineToken(0, ''),
new ViewLineToken(4, 'a'),
new ViewLineToken(6, 'b')
],
0,
false,
true,
[
new ViewLineToken(0, ' indent-guide'),
new ViewLineToken(4, 'a'),
new ViewLineToken(6, 'b')
]
);
});
test('createLineParts render indent guides - 8 leading spaces', () => {
testCreateLineParts(
' Hello world! ',
[
new ViewLineToken(0, ''),
new ViewLineToken(8, 'a'),
new ViewLineToken(10, 'b')
],
0,
false,
true,
[
new ViewLineToken(0, ' indent-guide'),
new ViewLineToken(4, ' indent-guide'),
new ViewLineToken(8, 'a'),
new ViewLineToken(10, 'b')
]
);
});
test('createLineParts render indent guides - 2 leading tabs', () => {
testCreateLineParts(
'\t\tHello world!\t',
[
new ViewLineToken(0, ''),
new ViewLineToken(2, 'a'),
new ViewLineToken(4, 'b')
],
0,
false,
true,
[
new ViewLineToken(0, ' indent-guide'),
new ViewLineToken(1, ' indent-guide'),
new ViewLineToken(2, 'a'),
new ViewLineToken(4, 'b'),
]
);
});
test('createLineParts render indent guides - mixed leading spaces and tabs', () => {
testCreateLineParts(
' \t\t Hello world! \t \t \t ',
[
new ViewLineToken(0, ''),
new ViewLineToken(6, 'a'),
new ViewLineToken(8, 'b')
],
0,
false,
true,
[
new ViewLineToken(0, ' indent-guide'),
new ViewLineToken(3, ' indent-guide'),
new ViewLineToken(4, ' indent-guide'),
new ViewLineToken(6, 'a'),
new ViewLineToken(8, 'b'),
]
);
});
test('createLineParts render whitespace and indent guides - mixed leading spaces and tabs', () => {
testCreateLineParts(
' \t\t Hello world! \t \t \t ',
[
new ViewLineToken(0, ''),
new ViewLineToken(6, 'a'),
new ViewLineToken(8, 'b')
],
0,
true,
true,
[
new ViewLineToken(0, ' leading whitespace indent-guide'),
new ViewLineToken(3, ' leading whitespace indent-guide'),
new ViewLineToken(4, ' leading whitespace indent-guide'),
new ViewLineToken(6, 'a'),
new ViewLineToken(8, 'b'),
new ViewLineToken(18, 'b trailing whitespace'),
new ViewLineToken(20, 'b trailing whitespace'),
new ViewLineToken(23, 'b trailing whitespace'),
new ViewLineToken(27, 'b trailing whitespace'),
]
);
});
test('createLineParts render whitespace skips faux indent', () => {
testCreateLineParts(
'\t\t Hello world! \t \t \t ',
[
new ViewLineToken(0, ''),
new ViewLineToken(4, 'a'),
new ViewLineToken(6, 'b')
],
2,
true,
false,
[
new ViewLineToken(0, ''),
new ViewLineToken(2, ' leading whitespace'),
new ViewLineToken(4, 'a'),
new ViewLineToken(6, 'b'),
new ViewLineToken(16, 'b trailing whitespace'),
new ViewLineToken(18, 'b trailing whitespace'),
new ViewLineToken(21, 'b trailing whitespace'),
new ViewLineToken(25, 'b trailing whitespace'),
]
);
});
test('createLineParts render indent guides ignores line with faux indent', () => {
testCreateLineParts(
'\t\t Hello world! \t \t \t ',
[
new ViewLineToken(0, ''),
new ViewLineToken(4, 'a'),
new ViewLineToken(6, 'b')
],
2,
false,
true,
[
new ViewLineToken(0, ''),
new ViewLineToken(4, 'a'),
new ViewLineToken(6, 'b')
]
);
});
test('createLineParts render whitespace and indent guides for line with faux indent', () => {
testCreateLineParts(
'\t\t Hello world! \t \t \t ',
[
new ViewLineToken(0, ''),
new ViewLineToken(4, 'a'),
new ViewLineToken(6, 'b')
],
2,
true,
true,
[
new ViewLineToken(0, ''),
new ViewLineToken(2, ' leading whitespace'),
new ViewLineToken(4, 'a'),
new ViewLineToken(6, 'b'),
new ViewLineToken(16, 'b trailing whitespace'),
new ViewLineToken(18, 'b trailing whitespace'),
new ViewLineToken(21, 'b trailing whitespace'),
new ViewLineToken(25, 'b trailing whitespace'),
]
);
});
test('ViewLineParts', () => {
assert.deepEqual(LineDecorationsNormalizer.normalize(1, 1, [
......
......@@ -347,6 +347,7 @@ function _processThemeObject(themeId: string, themeDocument: ThemeDocument): str
if (editorSettings.invisibles) {
let invisibles = new Color(editorSettings.invisibles);
cssRules.push(`.monaco-editor.${themeSelector} .token.whitespace { color: ${invisibles} !important; }`);
cssRules.push(`.monaco-editor.${themeSelector} .token.indent-guide { border-left: 1px solid ${invisibles}; }`);
}
return cssRules.join('\n');
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册