diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index dcfc1dc3ad3ba4165cc17c7f33407d0b8f7da300..6e1338d6adb803179cf9c55574c77354c4119cd4 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -191,6 +191,7 @@ export class ViewLine implements IVisibleLine { let renderLineInput = new RenderLineInput( options.useMonospaceOptimizations, lineData.content, + lineData.continuesWithWrappedLine, lineData.isBasicASCII, lineData.containsRTL, lineData.minColumn - 1, @@ -230,7 +231,7 @@ export class ViewLine implements IVisibleLine { // Another rounding error has been observed on Linux in VSCode, where width // rounding errors add up to an observable large number... // --- - // Also see another example of rounding errors on Windows in + // Also see another example of rounding errors on Windows in // https://github.com/Microsoft/vscode/issues/33178 renderedViewLine = new FastRenderedViewLine( this._renderedViewLine ? this._renderedViewLine.domNode : null, diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 12212dd75c2f864ff103fed15a61549938369188..c3b05628b1f3e789d540d1f3274ebbb1ad0781a9 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -1989,6 +1989,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { const output = renderViewLine(new RenderLineInput( (config.fontInfo.isMonospace && !config.viewInfo.disableMonospaceOptimizations), lineContent, + false, isBasicASCII, containsRTL, 0, diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index ad7a63bd821d05d0f8a16ff0cf99c2dbb55a678e..1270769cdca46f6371aab9461450a77d3035b1a9 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -769,6 +769,7 @@ export class DiffReview extends Disposable { const r = renderViewLine(new RenderLineInput( (config.fontInfo.isMonospace && !config.viewInfo.disableMonospaceOptimizations), lineContent, + false, isBasicASCII, containsRTL, 0, diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index d71efb8ecd93501656e0d9559664707ef826cbd0..977f9a5235b3664ec35b1b8e674af003b92b827c 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -36,6 +36,7 @@ export class RenderLineInput { public readonly useMonospaceOptimizations: boolean; public readonly lineContent: string; + public readonly continuesWithWrappedLine: boolean; public readonly isBasicASCII: boolean; public readonly containsRTL: boolean; public readonly fauxIndentLength: number; @@ -51,6 +52,7 @@ export class RenderLineInput { constructor( useMonospaceOptimizations: boolean, lineContent: string, + continuesWithWrappedLine: boolean, isBasicASCII: boolean, containsRTL: boolean, fauxIndentLength: number, @@ -65,6 +67,7 @@ export class RenderLineInput { ) { this.useMonospaceOptimizations = useMonospaceOptimizations; this.lineContent = lineContent; + this.continuesWithWrappedLine = continuesWithWrappedLine; this.isBasicASCII = isBasicASCII; this.containsRTL = containsRTL; this.fauxIndentLength = fauxIndentLength; @@ -88,6 +91,7 @@ export class RenderLineInput { return ( this.useMonospaceOptimizations === other.useMonospaceOptimizations && this.lineContent === other.lineContent + && this.continuesWithWrappedLine === other.continuesWithWrappedLine && this.isBasicASCII === other.isBasicASCII && this.containsRTL === other.containsRTL && this.fauxIndentLength === other.fauxIndentLength @@ -331,7 +335,7 @@ 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, tokens, input.fauxIndentLength, input.tabSize, useMonospaceOptimizations, input.renderWhitespace === RenderWhitespace.Boundary); + tokens = _applyRenderWhitespace(lineContent, len, input.continuesWithWrappedLine, tokens, input.fauxIndentLength, input.tabSize, useMonospaceOptimizations, input.renderWhitespace === RenderWhitespace.Boundary); } let containsForeignElements = ForeignElementType.None; if (input.lineDecorations.length > 0) { @@ -437,7 +441,7 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[]): LinePart[] { * Moreover, a token is created for every visual indent because on some fonts the glyphs used for rendering whitespace (→ or ·) do not have the same width as  . * The rendering phase will generate `style="width:..."` for these tokens. */ -function _applyRenderWhitespace(lineContent: string, len: number, 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, onlyBoundary: boolean): LinePart[] { let result: LinePart[] = [], resultLen = 0; let tokenIndex = 0; @@ -527,14 +531,23 @@ function _applyRenderWhitespace(lineContent: string, len: number, tokens: LinePa } } + let generateWhitespace = false; if (wasInWhitespace) { // was in whitespace token - result[resultLen++] = new LinePart(len, 'vs-whitespace'); - } else { - // was in regular token - result[resultLen++] = new LinePart(len, tokenType); + if (continuesWithWrappedLine && onlyBoundary) { + let lastCharCode = (len > 0 ? lineContent.charCodeAt(len - 1) : CharCode.Null); + let prevCharCode = (len > 1 ? lineContent.charCodeAt(len - 2) : CharCode.Null); + let isSingleTrailingSpace = (lastCharCode === CharCode.Space && (prevCharCode !== CharCode.Space && prevCharCode !== CharCode.Tab)); + if (!isSingleTrailingSpace) { + generateWhitespace = true; + } + } else { + generateWhitespace = true; + } } + result[resultLen++] = new LinePart(len, generateWhitespace ? 'vs-whitespace' : tokenType); + return result; } diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 0c780009714ae5a0b784a88d973a297453896afe..17ef4199216367f31ccb9f5dc5c14e47ab238a4a 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -884,6 +884,7 @@ class VisibleIdentitySplitLine implements ISplitLine { let lineContent = lineTokens.getLineContent(); return new ViewLineData( lineContent, + false, 1, lineContent.length + 1, lineTokens.inflate() @@ -1087,6 +1088,8 @@ export class SplitLine implements ISplitLine { let minColumn = (outputLineIndex > 0 ? this.wrappedIndentLength + 1 : 1); let maxColumn = lineContent.length + 1; + let continuesWithWrappedLine = (outputLineIndex + 1 < this.getViewLineCount()); + let deltaStartIndex = 0; if (outputLineIndex > 0) { deltaStartIndex = this.wrappedIndentLength; @@ -1095,6 +1098,7 @@ export class SplitLine implements ISplitLine { return new ViewLineData( lineContent, + continuesWithWrappedLine, minColumn, maxColumn, lineTokens.sliceAndInflate(startOffset, endOffset, deltaStartIndex) @@ -1318,6 +1322,7 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { let lineContent = lineTokens.getLineContent(); return new ViewLineData( lineContent, + false, 1, lineContent.length + 1, lineTokens.inflate() diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index 2ad4bb879a47509dbdab10160aa89d5aea7f5081..704063ed074e166c775b31f202fc7cccb4f888c5 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -172,6 +172,10 @@ export class ViewLineData { * The content at this view line. */ public readonly content: string; + /** + * Does this line continue with a wrapped line? + */ + public readonly continuesWithWrappedLine: boolean; /** * The minimum allowed column at this view line. */ @@ -187,11 +191,13 @@ export class ViewLineData { constructor( content: string, + continuesWithWrappedLine: boolean, minColumn: number, maxColumn: number, tokens: IViewLineTokens ) { this.content = content; + this.continuesWithWrappedLine = continuesWithWrappedLine; this.minColumn = minColumn; this.maxColumn = maxColumn; this.tokens = tokens; @@ -211,6 +217,10 @@ export class ViewLineRenderingData { * The content at this view line. */ public readonly content: string; + /** + * Does this line continue with a wrapped line? + */ + public readonly continuesWithWrappedLine: boolean; /** * Describes if `content` contains RTL characters. */ @@ -236,6 +246,7 @@ export class ViewLineRenderingData { minColumn: number, maxColumn: number, content: string, + continuesWithWrappedLine: boolean, mightContainRTL: boolean, mightContainNonBasicASCII: boolean, tokens: IViewLineTokens, @@ -245,6 +256,7 @@ export class ViewLineRenderingData { this.minColumn = minColumn; this.maxColumn = maxColumn; this.content = content; + this.continuesWithWrappedLine = continuesWithWrappedLine; this.isBasicASCII = ViewLineRenderingData.isBasicASCII(content, mightContainNonBasicASCII); this.containsRTL = ViewLineRenderingData.containsRTL(content, this.isBasicASCII, mightContainRTL); diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 00e49e8e91c42c2e8f1280fa3d4ef7c96c17a11e..5097fcafadfc92cd06aff21724145f22db31bf5f 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -508,6 +508,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel lineData.minColumn, lineData.maxColumn, lineData.content, + lineData.continuesWithWrappedLine, mightContainRTL, mightContainNonBasicASCII, lineData.tokens, diff --git a/src/vs/editor/standalone/browser/colorizer.ts b/src/vs/editor/standalone/browser/colorizer.ts index 61e3b4483927b7e3fe3b9c551c084a02c869a01e..298a4d7a0d5fd74e42ff3f415999284ed74f23d8 100644 --- a/src/vs/editor/standalone/browser/colorizer.ts +++ b/src/vs/editor/standalone/browser/colorizer.ts @@ -100,6 +100,7 @@ export class Colorizer { let renderResult = renderViewLine(new RenderLineInput( false, line, + false, isBasicASCII, containsRTL, 0, @@ -152,6 +153,7 @@ function _fakeColorize(lines: string[], tabSize: number): string { let renderResult = renderViewLine(new RenderLineInput( false, line, + false, isBasicASCII, containsRTL, 0, @@ -186,6 +188,7 @@ function _actualColorize(lines: string[], tabSize: number, tokenizationSupport: let renderResult = renderViewLine(new RenderLineInput( false, line, + false, isBasicASCII, containsRTL, 0, diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 8678813aee3714e6bb0940578e5360632f11efcb..59705ec0d930c83781b4b39b0231f8d7ff867cb1 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -30,6 +30,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, lineContent, + false, strings.isBasicASCII(lineContent), false, 0, @@ -77,6 +78,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, lineContent, + false, true, false, 0, @@ -114,6 +116,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, 'Hello world!', + false, true, false, 0, @@ -216,6 +219,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, lineText, + false, true, false, 0, @@ -276,6 +280,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, lineText, + false, true, false, 0, @@ -336,6 +341,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( false, lineText, + false, true, false, 0, @@ -374,6 +380,7 @@ suite('viewLineRenderer.renderLine', () => { false, lineText, false, + false, true, 0, lineParts, @@ -401,6 +408,7 @@ suite('viewLineRenderer.renderLine', () => { let actual = renderViewLine(new RenderLineInput( false, lineText, + false, true, false, 0, @@ -499,6 +507,7 @@ suite('viewLineRenderer.renderLine', () => { let actual = renderViewLine(new RenderLineInput( false, lineText, + false, true, false, 0, @@ -535,6 +544,7 @@ suite('viewLineRenderer.renderLine', () => { lineText, false, false, + false, 0, lineParts, [], @@ -561,6 +571,7 @@ suite('viewLineRenderer.renderLine', () => { false, lineText, false, + false, true, 0, lineParts, @@ -604,6 +615,7 @@ suite('viewLineRenderer.renderLine', () => { let _actual = renderViewLine(new RenderLineInput( true, lineText, + false, true, false, 4, @@ -685,6 +697,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( fontIsMonospace, lineContent, + false, true, false, fauxIndentLength, @@ -708,6 +721,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, lineContent, + false, true, false, 0, @@ -737,6 +751,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( true, lineContent, + false, true, false, 0, @@ -1002,6 +1017,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, 'Hello world', + false, true, false, 0, @@ -1044,6 +1060,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, lineContent, + false, true, false, 0, @@ -1074,6 +1091,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, lineContent, + false, true, false, 0, @@ -1105,6 +1123,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( false, lineContent, + false, true, false, 0, @@ -1134,6 +1153,7 @@ suite('viewLineRenderer.renderLine 2', () => { ' 1. 🙏', false, false, + false, 0, createViewLineTokens([createPart(7, 3)]), [new LineDecoration(7, 8, 'inline-folded', InlineDecorationType.After)], @@ -1160,6 +1180,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( true, '', + false, true, false, 0, @@ -1190,6 +1211,7 @@ suite('viewLineRenderer.renderLine 2', () => { let actual = renderViewLine(new RenderLineInput( true, '\t}', + false, true, false, 0, @@ -1223,6 +1245,7 @@ suite('viewLineRenderer.renderLine 2', () => { 'asd = "擦"\t\t#asd', false, false, + false, 0, createViewLineTokens([createPart(15, 3)]), [], @@ -1250,6 +1273,7 @@ suite('viewLineRenderer.renderLine 2', () => { 'asd = "擦"\t\t#asd', false, false, + false, 0, createViewLineTokens([createPart(15, 3)]), [], @@ -1283,6 +1307,7 @@ suite('viewLineRenderer.renderLine 2', () => { '12345689012345678901234568901234567890123456890abába', false, false, + false, 0, createViewLineTokens([createPart(53, 3)]), [], @@ -1310,6 +1335,7 @@ suite('viewLineRenderer.renderLine 2', () => { ' JoyShareல் பின்தொடர்ந்து, விடீயோ, ஜோக்குகள், அனிமேசன், நகைச்சுவை படங்கள் மற்றும் செய்திகளை பெறுவீர்', false, false, + false, 0, createViewLineTokens([createPart(100, 3)]), [], @@ -1341,6 +1367,7 @@ suite('viewLineRenderer.renderLine 2', () => { ' वो ऐसा क्या है जो हमारे अंदर भी है और बाहर भी है। जिसकी वजह से हम सब हैं। जिसने इस सृष्टि की रचना की है।', false, false, + false, 0, createViewLineTokens([createPart(105, 3)]), [], @@ -1361,10 +1388,38 @@ suite('viewLineRenderer.renderLine 2', () => { assert.deepEqual(actual.html, expected); }); + test('issue #38123: editor.renderWhitespace: "boundary" renders whitespace at line wrap point when line is wrapped', () => { + let actual = renderViewLine(new RenderLineInput( + true, + 'This is a long line which never uses more than two spaces. ', + true, + true, + false, + 0, + createViewLineTokens([createPart(59, 3)]), + [], + 4, + 10, + 10000, + 'boundary', + false, + false + )); + + let expected = [ + '', + 'This\u00a0is\u00a0a\u00a0long\u00a0line\u00a0which\u00a0never\u00a0uses\u00a0more\u00a0than\u00a0two\u00a0spaces.\u00a0', + '' + ].join(''); + + assert.deepEqual(actual.html, expected); + }); + function createTestGetColumnOfLinePartOffset(lineContent: string, tabSize: number, parts: ViewLineToken[], expectedPartLengths: number[]): (partIndex: number, partLength: number, offset: number, expected: number) => void { let renderLineOutput = renderViewLine(new RenderLineInput( false, lineContent, + false, true, false, 0,