Skip to content

Commit

Permalink
Add 'selection' option to render whitespace, closes #1477
Browse files Browse the repository at this point in the history
  • Loading branch information
Rachel Macfarlane authored Jul 22, 2019
1 parent 37e26c0 commit 765d681
Show file tree
Hide file tree
Showing 9 changed files with 324 additions and 58 deletions.
23 changes: 17 additions & 6 deletions src/vs/editor/browser/viewParts/lines/viewLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { IStringBuilder } from 'vs/editor/common/core/stringBuilder';
import { IConfiguration } from 'vs/editor/common/editorCommon';
import { HorizontalRange } from 'vs/editor/common/view/renderingContext';
import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations';
import { CharacterMapping, ForeignElementType, RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer';
import { CharacterMapping, ForeignElementType, RenderLineInput, renderViewLine, LineRange } from 'vs/editor/common/viewLayout/viewLineRenderer';
import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData';
import { InlineDecorationType } from 'vs/editor/common/viewModel/viewModel';
import { HIGH_CONTRAST, ThemeType } from 'vs/platform/theme/common/themeService';
Expand Down Expand Up @@ -69,7 +69,7 @@ export class DomReadingContext {

export class ViewLineOptions {
public readonly themeType: ThemeType;
public readonly renderWhitespace: 'none' | 'boundary' | 'all';
public readonly renderWhitespace: 'none' | 'boundary' | 'selection' | 'all';
public readonly renderControlCharacters: boolean;
public readonly spaceWidth: number;
public readonly useMonospaceOptimizations: boolean;
Expand Down Expand Up @@ -152,7 +152,7 @@ export class ViewLine implements IVisibleLine {
this._options = newOptions;
}
public onSelectionChanged(): boolean {
if (alwaysRenderInlineSelection || this._options.themeType === HIGH_CONTRAST) {
if (alwaysRenderInlineSelection || this._options.themeType === HIGH_CONTRAST || this._options.renderWhitespace === 'selection') {
this._isMaybeInvalid = true;
return true;
}
Expand All @@ -171,7 +171,9 @@ export class ViewLine implements IVisibleLine {
const options = this._options;
const actualInlineDecorations = LineDecoration.filter(lineData.inlineDecorations, lineNumber, lineData.minColumn, lineData.maxColumn);

if (alwaysRenderInlineSelection || options.themeType === HIGH_CONTRAST) {
// Only send selection information when needed for rendering whitespace
let selectionsOnLine: LineRange[] | null = null;
if (alwaysRenderInlineSelection || options.themeType === HIGH_CONTRAST || this._options.renderWhitespace === 'selection') {
const selections = viewportData.selections;
for (const selection of selections) {

Expand All @@ -184,7 +186,15 @@ export class ViewLine implements IVisibleLine {
const endColumn = (selection.endLineNumber === lineNumber ? selection.endColumn : lineData.maxColumn);

if (startColumn < endColumn) {
actualInlineDecorations.push(new LineDecoration(startColumn, endColumn, 'inline-selected-text', InlineDecorationType.Regular));
if (this._options.renderWhitespace !== 'selection') {
actualInlineDecorations.push(new LineDecoration(startColumn, endColumn, 'inline-selected-text', InlineDecorationType.Regular));
} else {
if (!selectionsOnLine) {
selectionsOnLine = [];
}

selectionsOnLine.push(new LineRange(startColumn - 1, endColumn - 1));
}
}
}
}
Expand All @@ -204,7 +214,8 @@ export class ViewLine implements IVisibleLine {
options.stopRenderingLineAfter,
options.renderWhitespace,
options.renderControlCharacters,
options.fontLigatures
options.fontLigatures,
selectionsOnLine
);

if (this._renderedViewLine && this._renderedViewLine.input.equals(renderLineInput)) {
Expand Down
3 changes: 2 additions & 1 deletion src/vs/editor/browser/widget/diffEditorWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2027,7 +2027,8 @@ class InlineViewZonesComputer extends ViewZonesComputer {
config.viewInfo.stopRenderingLineAfter,
config.viewInfo.renderWhitespace,
config.viewInfo.renderControlCharacters,
config.viewInfo.fontLigatures
config.viewInfo.fontLigatures,
null // Send no selections, original line cannot be selected
), sb);

sb.appendASCIIString('</div>');
Expand Down
3 changes: 2 additions & 1 deletion src/vs/editor/browser/widget/diffReview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,8 @@ export class DiffReview extends Disposable {
config.viewInfo.stopRenderingLineAfter,
config.viewInfo.renderWhitespace,
config.viewInfo.renderControlCharacters,
config.viewInfo.fontLigatures
config.viewInfo.fontLigatures,
null
));

return r.html;
Expand Down
3 changes: 2 additions & 1 deletion src/vs/editor/common/config/commonEditorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -901,10 +901,11 @@ const editorConfiguration: IConfigurationNode = {
},
'editor.renderWhitespace': {
'type': 'string',
'enum': ['none', 'boundary', 'all'],
'enum': ['none', 'boundary', 'selection', 'all'],
'enumDescriptions': [
'',
nls.localize('renderWhiteSpace.boundary', "Render whitespace characters except for single spaces between words."),
nls.localize('renderWhitespace.selection', "Render whitespace characters only on selected text."),
''
],
default: EDITOR_DEFAULTS.viewInfo.renderWhitespace,
Expand Down
6 changes: 3 additions & 3 deletions src/vs/editor/common/config/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ export interface IEditorOptions {
* Enable rendering of whitespace.
* Defaults to none.
*/
renderWhitespace?: 'none' | 'boundary' | 'all';
renderWhitespace?: 'none' | 'boundary' | 'selection' | 'all';
/**
* Enable rendering of control characters.
* Defaults to false.
Expand Down Expand Up @@ -999,7 +999,7 @@ export interface InternalEditorViewOptions {
readonly scrollBeyondLastColumn: number;
readonly smoothScrolling: boolean;
readonly stopRenderingLineAfter: number;
readonly renderWhitespace: 'none' | 'boundary' | 'all';
readonly renderWhitespace: 'none' | 'boundary' | 'selection' | 'all';
readonly renderControlCharacters: boolean;
readonly fontLigatures: boolean;
readonly renderIndentGuides: boolean;
Expand Down Expand Up @@ -2017,7 +2017,7 @@ export class EditorOptionsValidator {
} else if (<any>renderWhitespace === false) {
renderWhitespace = 'none';
}
renderWhitespace = _stringSet<'none' | 'boundary' | 'all'>(renderWhitespace, defaults.renderWhitespace, ['none', 'boundary', 'all']);
renderWhitespace = _stringSet<'none' | 'boundary' | 'selection' | 'all'>(renderWhitespace, defaults.renderWhitespace, ['none', 'boundary', 'selection', 'all']);
}

let renderLineHighlight = opts.renderLineHighlight;
Expand Down
83 changes: 75 additions & 8 deletions src/vs/editor/common/viewLayout/viewLineRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { InlineDecorationType } from 'vs/editor/common/viewModel/viewModel';
export const enum RenderWhitespace {
None = 0,
Boundary = 1,
All = 2
Selection = 2,
All = 3
}

class LinePart {
Expand All @@ -31,6 +32,28 @@ class LinePart {
}
}

export class LineRange {
/**
* Zero-based offset on which the range starts, inclusive.
*/
public readonly startOffset: number;

/**
* Zero-based offset on which the range ends, inclusive.
*/
public readonly endOffset: number;

constructor(startIndex: number, endIndex: number) {
this.startOffset = startIndex;
this.endOffset = endIndex;
}

public equals(otherLineRange: LineRange) {
return this.startOffset === otherLineRange.startOffset
&& this.endOffset === otherLineRange.endOffset;
}
}

export class RenderLineInput {

public readonly useMonospaceOptimizations: boolean;
Expand All @@ -49,6 +72,12 @@ export class RenderLineInput {
public readonly renderControlCharacters: boolean;
public readonly fontLigatures: boolean;

/**
* Defined only when renderWhitespace is 'selection'. Selections are non-overlapping,
* and ordered by position within the line.
*/
public readonly selectionsOnLine: LineRange[] | null;

constructor(
useMonospaceOptimizations: boolean,
canUseHalfwidthRightwardsArrow: boolean,
Expand All @@ -62,9 +91,10 @@ export class RenderLineInput {
tabSize: number,
spaceWidth: number,
stopRenderingLineAfter: number,
renderWhitespace: 'none' | 'boundary' | 'all',
renderWhitespace: 'none' | 'boundary' | 'selection' | 'all',
renderControlCharacters: boolean,
fontLigatures: boolean
fontLigatures: boolean,
selectionsOnLine: LineRange[] | null
) {
this.useMonospaceOptimizations = useMonospaceOptimizations;
this.canUseHalfwidthRightwardsArrow = canUseHalfwidthRightwardsArrow;
Expand All @@ -83,10 +113,35 @@ export class RenderLineInput {
? RenderWhitespace.All
: renderWhitespace === 'boundary'
? RenderWhitespace.Boundary
: RenderWhitespace.None
: renderWhitespace === 'selection'
? RenderWhitespace.Selection
: RenderWhitespace.None
);
this.renderControlCharacters = renderControlCharacters;
this.fontLigatures = fontLigatures;
this.selectionsOnLine = selectionsOnLine && selectionsOnLine.sort((a, b) => a.startOffset < b.startOffset ? -1 : 1);
}

private sameSelection(otherSelections: LineRange[] | null): boolean {
if (this.selectionsOnLine === null) {
return otherSelections === null;
}

if (otherSelections === null) {
return false;
}

if (otherSelections.length !== this.selectionsOnLine.length) {
return false;
}

for (let i = 0; i < this.selectionsOnLine.length; i++) {
if (!this.selectionsOnLine[i].equals(otherSelections[i])) {
return false;
}
}

return true;
}

public equals(other: RenderLineInput): boolean {
Expand All @@ -106,6 +161,7 @@ export class RenderLineInput {
&& this.fontLigatures === other.fontLigatures
&& LineDecoration.equalsArr(this.lineDecorations, other.lineDecorations)
&& this.lineTokens.equals(other.lineTokens)
&& this.sameSelection(other.selectionsOnLine)
);
}
}
Expand Down Expand Up @@ -338,8 +394,8 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput
}

let tokens = transformAndRemoveOverflowing(input.lineTokens, input.fauxIndentLength, len);
if (input.renderWhitespace === RenderWhitespace.All || input.renderWhitespace === RenderWhitespace.Boundary) {
tokens = _applyRenderWhitespace(lineContent, len, input.continuesWithWrappedLine, tokens, input.fauxIndentLength, input.tabSize, useMonospaceOptimizations, input.renderWhitespace === RenderWhitespace.Boundary);
if (input.renderWhitespace === RenderWhitespace.All || input.renderWhitespace === RenderWhitespace.Boundary || (input.renderWhitespace === RenderWhitespace.Selection && !!input.selectionsOnLine)) {
tokens = _applyRenderWhitespace(lineContent, len, input.continuesWithWrappedLine, tokens, input.fauxIndentLength, input.tabSize, useMonospaceOptimizations, input.selectionsOnLine, input.renderWhitespace === RenderWhitespace.Boundary);
}
let containsForeignElements = ForeignElementType.None;
if (input.lineDecorations.length > 0) {
Expand Down Expand Up @@ -481,7 +537,7 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces:
* Moreover, a token is created for every visual indent because on some fonts the glyphs used for rendering whitespace (&rarr; or &middot;) do not have the same width as &nbsp;.
* The rendering phase will generate `style="width:..."` for these tokens.
*/
function _applyRenderWhitespace(lineContent: string, len: number, continuesWithWrappedLine: boolean, tokens: LinePart[], fauxIndentLength: number, tabSize: number, useMonospaceOptimizations: boolean, onlyBoundary: boolean): LinePart[] {
function _applyRenderWhitespace(lineContent: string, len: number, continuesWithWrappedLine: boolean, tokens: LinePart[], fauxIndentLength: number, tabSize: number, useMonospaceOptimizations: boolean, selections: LineRange[] | null, onlyBoundary: boolean): LinePart[] {

let result: LinePart[] = [], resultLen = 0;
let tokenIndex = 0;
Expand Down Expand Up @@ -511,11 +567,17 @@ function _applyRenderWhitespace(lineContent: string, len: number, continuesWithW
}
}
tmpIndent = tmpIndent % tabSize;

let wasInWhitespace = false;
let currentSelectionIndex = 0;
let currentSelection = selections && selections[currentSelectionIndex];
for (let charIndex = fauxIndentLength; charIndex < len; charIndex++) {
const chCode = lineContent.charCodeAt(charIndex);

if (currentSelection && charIndex >= currentSelection.endOffset) {
currentSelectionIndex++;
currentSelection = selections && selections[currentSelectionIndex];
}

let isInWhitespace: boolean;
if (charIndex < firstNonWhitespaceIndex || charIndex > lastNonWhitespaceIndex) {
// in leading or trailing whitespace
Expand All @@ -540,6 +602,11 @@ function _applyRenderWhitespace(lineContent: string, len: number, continuesWithW
isInWhitespace = false;
}

// If rendering whitespace on selection, check that the charIndex falls within a selection
if (isInWhitespace && selections) {
isInWhitespace = !!currentSelection && currentSelection.startOffset <= charIndex && currentSelection.endOffset > charIndex;
}

if (wasInWhitespace) {
// was in whitespace token
if (!isInWhitespace || (!useMonospaceOptimizations && tmpIndent >= tabSize)) {
Expand Down
9 changes: 6 additions & 3 deletions src/vs/editor/standalone/browser/colorizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ export class Colorizer {
-1,
'none',
false,
false
false,
null
));
return renderResult.html;
}
Expand Down Expand Up @@ -195,7 +196,8 @@ function _fakeColorize(lines: string[], tabSize: number): string {
-1,
'none',
false,
false
false,
null
));

html = html.concat(renderResult.html);
Expand Down Expand Up @@ -231,7 +233,8 @@ function _actualColorize(lines: string[], tabSize: number, tokenizationSupport:
-1,
'none',
false,
false
false,
null
));

html = html.concat(renderResult.html);
Expand Down
Loading

0 comments on commit 765d681

Please sign in to comment.