diff --git a/src/vs/base/common/uint.ts b/src/vs/base/common/uint.ts index b44e0fdaec7bdeed12623a4bb1843634cff83486..347af57eec20d0dbaf6380784408a32dca65a327 100644 --- a/src/vs/base/common/uint.ts +++ b/src/vs/base/common/uint.ts @@ -57,12 +57,3 @@ export function toUint32(v: number): number { } return v | 0; } - -export function toUint32Array(arr: number[]): Uint32Array { - const len = arr.length; - const r = new Uint32Array(len); - for (let i = 0; i < len; i++) { - r[i] = toUint32(arr[i]); - } - return r; -} diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index 6af61888a62b89d96c70644765bad1bcdae0ef86..33979994f9deefe4ee1572ad2eed12848dc93c87 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -136,14 +136,16 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const indent = indents[lineIndex]; let result = ''; - const leftMostVisiblePosition = ctx.visibleRangeForPosition(new Position(lineNumber, 1)); - let left = leftMostVisiblePosition ? leftMostVisiblePosition.left : 0; - for (let i = 1; i <= indent; i++) { - const className = (containsActiveIndentGuide && i === activeIndentLevel ? 'cigra' : 'cigr'); - result += `
`; - left += indentWidth; - if (left > scrollWidth || (this._maxIndentLeft > 0 && left > this._maxIndentLeft)) { - break; + if (indent >= 1) { + const leftMostVisiblePosition = ctx.visibleRangeForPosition(new Position(lineNumber, 1)); + let left = leftMostVisiblePosition ? leftMostVisiblePosition.left : 0; + for (let i = 1; i <= indent; i++) { + const className = (containsActiveIndentGuide && i === activeIndentLevel ? 'cigra' : 'cigr'); + result += `
`; + left += indentWidth; + if (left > scrollWidth || (this._maxIndentLeft > 0 && left > this._maxIndentLeft)) { + break; + } } } diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 8840a0140322ede4032a829fce7c296164298a92..cbe9a08f45fad419eee010de7fbec305fec51e88 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -213,6 +213,7 @@ export class ViewLine implements IVisibleLine { lineData.tokens, actualInlineDecorations, lineData.tabSize, + lineData.startVisibleColumn, options.spaceWidth, options.stopRenderingLineAfter, options.renderWhitespace, diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 9c84e1a8062fe8556d52036e810930c39e6f601e..ddfaa481492be4d450f9f1d99edc0f1c4c1ceb75 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -50,6 +50,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; let EDITOR_ID = 0; @@ -1329,7 +1330,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE model.onBeforeAttached(); - const viewModel = new ViewModel(this._id, this._configuration, model, (callback) => dom.scheduleAtNextAnimationFrame(callback)); + const viewModel = new ViewModel(this._id, this._configuration, model, MonospaceLineBreaksComputerFactory.create(this._configuration.options), (callback) => dom.scheduleAtNextAnimationFrame(callback)); listenersToRemove.push(model.onDidChangeDecorations((e) => this._onDidChangeModelDecorations.fire(e))); listenersToRemove.push(model.onDidChangeLanguage((e) => { diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 91b917320336813b59ae396263cd9b3d0e10c661..e6ae832954a33edc78870ddbe903ae2f3bd45b5c 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -2144,6 +2144,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { lineTokens, actualDecorations, tabSize, + 0, fontInfo.spaceWidth, options.get(EditorOption.stopRenderingLineAfter), options.get(EditorOption.renderWhitespace), diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index b0437b6d47fb94409db78a7aedc891b3fd92ff2c..4ac1919a690eacae4ec98bdd63d1d8bf204d5efb 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -780,6 +780,7 @@ export class DiffReview extends Disposable { lineTokens, [], tabSize, + 0, fontInfo.spaceWidth, options.get(EditorOption.stopRenderingLineAfter), options.get(EditorOption.renderWhitespace), diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 3adac8cf46b9299f5224ef8fc4a9351b6cc00587..94af6b10d1b559f3face94ba2254e20f528ad659 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -264,19 +264,14 @@ export interface IEditorOptions { wrappingIndent?: 'none' | 'same' | 'indent' | 'deepIndent'; /** * Configure word wrapping characters. A break will be introduced before these characters. - * Defaults to '{([+'. + * Defaults to '([{‘“〈《「『【〔([{「£¥$£¥++'. */ wordWrapBreakBeforeCharacters?: string; /** * Configure word wrapping characters. A break will be introduced after these characters. - * Defaults to ' \t})]?|&,;'. + * Defaults to ' \t})]?|/&.,;¢°′″‰℃、。。、¢,.:;?!%・・ゝゞヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻ァィゥェォャュョッー”〉》」』】〕)]}」'. */ wordWrapBreakAfterCharacters?: string; - /** - * Configure word wrapping characters. A break will be introduced after these characters only if no `wordWrapBreakBeforeCharacters` or `wordWrapBreakAfterCharacters` were found. - * Defaults to '.'. - */ - wordWrapBreakObtrusiveCharacters?: string; /** * Performance guard: Stop rendering a line after x characters. * Defaults to 10000. @@ -3154,7 +3149,6 @@ export const enum EditorOption { wordWrap, wordWrapBreakAfterCharacters, wordWrapBreakBeforeCharacters, - wordWrapBreakObtrusiveCharacters, wordWrapColumn, wordWrapMinified, wrappingIndent, @@ -3681,16 +3675,12 @@ export const EditorOptions = { )), wordWrapBreakAfterCharacters: register(new EditorStringOption( EditorOption.wordWrapBreakAfterCharacters, 'wordWrapBreakAfterCharacters', - ' \t})]?|/&,;¢°′″‰℃、。。、¢,.:;?!%・・ゝゞヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻ァィゥェォャュョッー”〉》」』】〕)]}」', + ' \t})]?|/&.,;¢°′″‰℃、。。、¢,.:;?!%・・ゝゞヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻ァィゥェォャュョッー”〉》」』】〕)]}」', )), wordWrapBreakBeforeCharacters: register(new EditorStringOption( EditorOption.wordWrapBreakBeforeCharacters, 'wordWrapBreakBeforeCharacters', '([{‘“〈《「『【〔([{「£¥$£¥++' )), - wordWrapBreakObtrusiveCharacters: register(new EditorStringOption( - EditorOption.wordWrapBreakObtrusiveCharacters, 'wordWrapBreakObtrusiveCharacters', - '.' - )), wordWrapColumn: register(new EditorIntOption( EditorOption.wordWrapColumn, 'wordWrapColumn', 80, 1, Constants.MAX_SAFE_SMALL_INTEGER, diff --git a/src/vs/editor/common/core/characterClassifier.ts b/src/vs/editor/common/core/characterClassifier.ts index 214bb2abf05330a5241021a4014f13ed6ece7bff..e5aac27c816bb66f1f461171ef03adbcfa45e9a4 100644 --- a/src/vs/editor/common/core/characterClassifier.ts +++ b/src/vs/editor/common/core/characterClassifier.ts @@ -12,14 +12,14 @@ export class CharacterClassifier { /** * Maintain a compact (fully initialized ASCII map for quickly classifying ASCII characters - used more often in code). */ - private _asciiMap: Uint8Array; + protected _asciiMap: Uint8Array; /** * The entire map (sparse array). */ - private _map: Map; + protected _map: Map; - private _defaultValue: number; + protected _defaultValue: number; constructor(_defaultValue: T) { let defaultValue = toUint8(_defaultValue); diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index 1c844dd66c4bb8fbce82856e2391808a7631e64a..ac23e98889c5638713a98f96d290e15594c0bc99 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -511,7 +511,93 @@ export class PieceTreeBase { } public getLinesContent(): string[] { - return this.getContentOfSubTree(this.root).split(/\r\n|\r|\n/); + let lines: string[] = []; + let linesLength = 0; + let currentLine = ''; + let danglingCR = false; + + this.iterate(this.root, node => { + if (node === SENTINEL) { + return true; + } + + const piece = node.piece; + let pieceLength = piece.length; + if (pieceLength === 0) { + return true; + } + + const buffer = this._buffers[piece.bufferIndex].buffer; + const lineStarts = this._buffers[piece.bufferIndex].lineStarts; + + const pieceStartLine = piece.start.line; + const pieceEndLine = piece.end.line; + let pieceStartOffset = lineStarts[pieceStartLine] + piece.start.column; + + if (danglingCR) { + if (buffer.charCodeAt(pieceStartOffset) === CharCode.LineFeed) { + // pretend the \n was in the previous piece.. + pieceStartOffset++; + pieceLength--; + } + lines[linesLength++] = currentLine; + currentLine = ''; + danglingCR = false; + if (pieceLength === 0) { + return true; + } + } + + if (pieceStartLine === pieceEndLine) { + // this piece has no new lines + if (!this._EOLNormalized && buffer.charCodeAt(pieceStartOffset + pieceLength - 1) === CharCode.CarriageReturn) { + danglingCR = true; + currentLine += buffer.substr(pieceStartOffset, pieceLength - 1); + } else { + currentLine += buffer.substr(pieceStartOffset, pieceLength); + } + return true; + } + + // add the text before the first line start in this piece + currentLine += ( + this._EOLNormalized + ? buffer.substring(pieceStartOffset, Math.max(pieceStartOffset, lineStarts[pieceStartLine + 1] - this._EOLLength)) + : buffer.substring(pieceStartOffset, lineStarts[pieceStartLine + 1]).replace(/(\r\n|\r|\n)$/, '') + ); + lines[linesLength++] = currentLine; + + for (let line = pieceStartLine + 1; line < pieceEndLine; line++) { + currentLine = ( + this._EOLNormalized + ? buffer.substring(lineStarts[line], lineStarts[line + 1] - this._EOLLength) + : buffer.substring(lineStarts[line], lineStarts[line + 1]).replace(/(\r\n|\r|\n)$/, '') + ); + lines[linesLength++] = currentLine; + } + + if (!this._EOLNormalized && buffer.charCodeAt(lineStarts[pieceEndLine] + piece.end.column - 1) === CharCode.CarriageReturn) { + danglingCR = true; + if (piece.end.column === 0) { + // The last line ended with a \r, let's undo the push, it will be pushed by next iteration + linesLength--; + } else { + currentLine = buffer.substr(lineStarts[pieceEndLine], piece.end.column - 1); + } + } else { + currentLine = buffer.substr(lineStarts[pieceEndLine], piece.end.column); + } + + return true; + }); + + if (danglingCR) { + lines[linesLength++] = currentLine; + currentLine = ''; + } + + lines[linesLength++] = currentLine; + return lines; } public getLength(): number { @@ -728,7 +814,7 @@ export class PieceTreeBase { // #endregion // #region Piece Table - insert(offset: number, value: string, eolNormalized: boolean = false): void { + public insert(offset: number, value: string, eolNormalized: boolean = false): void { this._EOLNormalized = this._EOLNormalized && eolNormalized; this._lastVisitedLine.lineNumber = 0; this._lastVisitedLine.value = ''; @@ -826,7 +912,7 @@ export class PieceTreeBase { this.computeBufferMetadata(); } - delete(offset: number, cnt: number): void { + public delete(offset: number, cnt: number): void { this._lastVisitedLine.lineNumber = 0; this._lastVisitedLine.value = ''; @@ -899,7 +985,7 @@ export class PieceTreeBase { this.computeBufferMetadata(); } - insertContentToNodeLeft(value: string, node: TreeNode) { + private insertContentToNodeLeft(value: string, node: TreeNode) { // we are inserting content to the beginning of node let nodesToDel: TreeNode[] = []; if (this.shouldCheckCRLF() && this.endWithCR(value) && this.startWithLF(node)) { @@ -934,7 +1020,7 @@ export class PieceTreeBase { this.deleteNodes(nodesToDel); } - insertContentToNodeRight(value: string, node: TreeNode) { + private insertContentToNodeRight(value: string, node: TreeNode) { // we are inserting to the right of this node. if (this.adjustCarriageReturnFromNext(value, node)) { // move \n to the new node. @@ -952,9 +1038,9 @@ export class PieceTreeBase { this.validateCRLFWithPrevNode(newNode); } - positionInBuffer(node: TreeNode, remainder: number): BufferCursor; - positionInBuffer(node: TreeNode, remainder: number, ret: BufferCursor): null; - positionInBuffer(node: TreeNode, remainder: number, ret?: BufferCursor): BufferCursor | null { + private positionInBuffer(node: TreeNode, remainder: number): BufferCursor; + private positionInBuffer(node: TreeNode, remainder: number, ret: BufferCursor): null; + private positionInBuffer(node: TreeNode, remainder: number, ret?: BufferCursor): BufferCursor | null { let piece = node.piece; let bufferIndex = node.piece.bufferIndex; let lineStarts = this._buffers[bufferIndex].lineStarts; @@ -1002,7 +1088,7 @@ export class PieceTreeBase { }; } - getLineFeedCnt(bufferIndex: number, start: BufferCursor, end: BufferCursor): number { + private getLineFeedCnt(bufferIndex: number, start: BufferCursor, end: BufferCursor): number { // we don't need to worry about start: abc\r|\n, or abc|\r, or abc|\n, or abc|\r\n doesn't change the fact that, there is one line break after start. // now let's take care of end: abc\r|\n, if end is in between \r and \n, we need to add line feed count by 1 if (end.column === 0) { @@ -1032,18 +1118,18 @@ export class PieceTreeBase { } } - offsetInBuffer(bufferIndex: number, cursor: BufferCursor): number { + private offsetInBuffer(bufferIndex: number, cursor: BufferCursor): number { let lineStarts = this._buffers[bufferIndex].lineStarts; return lineStarts[cursor.line] + cursor.column; } - deleteNodes(nodes: TreeNode[]): void { + private deleteNodes(nodes: TreeNode[]): void { for (let i = 0; i < nodes.length; i++) { rbDelete(this, nodes[i]); } } - createNewPieces(text: string): Piece[] { + private createNewPieces(text: string): Piece[] { if (text.length > AverageBufferSize) { // the content is large, operations like substring, charCode becomes slow // so here we split it into smaller chunks, just like what we did for CR/LF normalization @@ -1128,11 +1214,11 @@ export class PieceTreeBase { return [newPiece]; } - getLinesRawContent(): string { + public getLinesRawContent(): string { return this.getContentOfSubTree(this.root); } - getLineRawContent(lineNumber: number, endOffset: number = 0): string { + public getLineRawContent(lineNumber: number, endOffset: number = 0): string { let x = this.root; let ret = ''; @@ -1204,7 +1290,7 @@ export class PieceTreeBase { return ret; } - computeBufferMetadata() { + private computeBufferMetadata() { let x = this.root; let lfCnt = 1; @@ -1222,7 +1308,7 @@ export class PieceTreeBase { } // #region node operations - getIndexOf(node: TreeNode, accumulatedValue: number): { index: number, remainder: number } { + private getIndexOf(node: TreeNode, accumulatedValue: number): { index: number, remainder: number } { let piece = node.piece; let pos = this.positionInBuffer(node, accumulatedValue); let lineCnt = pos.line - piece.start.line; @@ -1239,7 +1325,7 @@ export class PieceTreeBase { return { index: lineCnt, remainder: pos.column }; } - getAccumulatedValue(node: TreeNode, index: number) { + private getAccumulatedValue(node: TreeNode, index: number) { if (index < 0) { return 0; } @@ -1253,7 +1339,7 @@ export class PieceTreeBase { } } - deleteNodeTail(node: TreeNode, pos: BufferCursor) { + private deleteNodeTail(node: TreeNode, pos: BufferCursor) { const piece = node.piece; const originalLFCnt = piece.lineFeedCnt; const originalEndOffset = this.offsetInBuffer(piece.bufferIndex, piece.end); @@ -1277,7 +1363,7 @@ export class PieceTreeBase { updateTreeMetadata(this, node, size_delta, lf_delta); } - deleteNodeHead(node: TreeNode, pos: BufferCursor) { + private deleteNodeHead(node: TreeNode, pos: BufferCursor) { const piece = node.piece; const originalLFCnt = piece.lineFeedCnt; const originalStartOffset = this.offsetInBuffer(piece.bufferIndex, piece.start); @@ -1299,7 +1385,7 @@ export class PieceTreeBase { updateTreeMetadata(this, node, size_delta, lf_delta); } - shrinkNode(node: TreeNode, start: BufferCursor, end: BufferCursor) { + private shrinkNode(node: TreeNode, start: BufferCursor, end: BufferCursor) { const piece = node.piece; const originalStartPos = piece.start; const originalEndPos = piece.end; @@ -1334,7 +1420,7 @@ export class PieceTreeBase { this.validateCRLFWithPrevNode(newNode); } - appendToNode(node: TreeNode, value: string): void { + private appendToNode(node: TreeNode, value: string): void { if (this.adjustCarriageReturnFromNext(value, node)) { value += '\n'; } @@ -1374,7 +1460,7 @@ export class PieceTreeBase { updateTreeMetadata(this, node, value.length, lf_delta); } - nodeAt(offset: number): NodePosition { + private nodeAt(offset: number): NodePosition { let x = this.root; let cache = this._searchCache.get(offset); if (cache) { @@ -1409,7 +1495,7 @@ export class PieceTreeBase { return null!; } - nodeAt2(lineNumber: number, column: number): NodePosition { + private nodeAt2(lineNumber: number, column: number): NodePosition { let x = this.root; let nodeStartOffset = 0; @@ -1476,7 +1562,7 @@ export class PieceTreeBase { return null!; } - nodeCharCodeAt(node: TreeNode, offset: number): number { + private nodeCharCodeAt(node: TreeNode, offset: number): number { if (node.piece.lineFeedCnt < 1) { return -1; } @@ -1485,7 +1571,7 @@ export class PieceTreeBase { return buffer.buffer.charCodeAt(newOffset); } - offsetOfNode(node: TreeNode): number { + private offsetOfNode(node: TreeNode): number { if (!node) { return 0; } @@ -1504,11 +1590,11 @@ export class PieceTreeBase { // #endregion // #region CRLF - shouldCheckCRLF() { + private shouldCheckCRLF() { return !(this._EOLNormalized && this._EOL === '\n'); } - startWithLF(val: string | TreeNode): boolean { + private startWithLF(val: string | TreeNode): boolean { if (typeof val === 'string') { return val.charCodeAt(0) === 10; } @@ -1532,7 +1618,7 @@ export class PieceTreeBase { return this._buffers[piece.bufferIndex].buffer.charCodeAt(startOffset) === 10; } - endWithCR(val: string | TreeNode): boolean { + private endWithCR(val: string | TreeNode): boolean { if (typeof val === 'string') { return val.charCodeAt(val.length - 1) === 13; } @@ -1544,7 +1630,7 @@ export class PieceTreeBase { return this.nodeCharCodeAt(val, val.piece.length - 1) === 13; } - validateCRLFWithPrevNode(nextNode: TreeNode) { + private validateCRLFWithPrevNode(nextNode: TreeNode) { if (this.shouldCheckCRLF() && this.startWithLF(nextNode)) { let node = nextNode.prev(); if (this.endWithCR(node)) { @@ -1553,7 +1639,7 @@ export class PieceTreeBase { } } - validateCRLFWithNextNode(node: TreeNode) { + private validateCRLFWithNextNode(node: TreeNode) { if (this.shouldCheckCRLF() && this.endWithCR(node)) { let nextNode = node.next(); if (this.startWithLF(nextNode)) { @@ -1562,7 +1648,7 @@ export class PieceTreeBase { } } - fixCRLF(prev: TreeNode, next: TreeNode) { + private fixCRLF(prev: TreeNode, next: TreeNode) { let nodesToDel: TreeNode[] = []; // update node let lineStarts = this._buffers[prev.piece.bufferIndex].lineStarts; @@ -1617,7 +1703,7 @@ export class PieceTreeBase { } } - adjustCarriageReturnFromNext(value: string, node: TreeNode): boolean { + private adjustCarriageReturnFromNext(value: string, node: TreeNode): boolean { if (this.shouldCheckCRLF() && this.endWithCR(value)) { let nextNode = node.next(); if (this.startWithLF(nextNode)) { @@ -1667,7 +1753,7 @@ export class PieceTreeBase { return callback(node) && this.iterate(node.right, callback); } - getNodeContent(node: TreeNode) { + private getNodeContent(node: TreeNode) { if (node === SENTINEL) { return ''; } @@ -1695,7 +1781,7 @@ export class PieceTreeBase { * / * z */ - rbInsertRight(node: TreeNode | null, p: Piece): TreeNode { + private rbInsertRight(node: TreeNode | null, p: Piece): TreeNode { let z = new TreeNode(p, NodeColor.Red); z.left = SENTINEL; z.right = SENTINEL; @@ -1727,7 +1813,7 @@ export class PieceTreeBase { * \ * z */ - rbInsertLeft(node: TreeNode | null, p: Piece): TreeNode { + private rbInsertLeft(node: TreeNode | null, p: Piece): TreeNode { let z = new TreeNode(p, NodeColor.Red); z.left = SENTINEL; z.right = SENTINEL; @@ -1751,7 +1837,7 @@ export class PieceTreeBase { return z; } - getContentOfSubTree(node: TreeNode): string { + private getContentOfSubTree(node: TreeNode): string { let str = ''; this.iterate(node, node => { diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 5298d234529bc80d18db5ab1ef05527bf849ed67..fec4ba1dfb3a00169dd7be359d3d3bc20c31acc0 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -66,6 +66,7 @@ export class RenderLineInput { public readonly lineTokens: IViewLineTokens; public readonly lineDecorations: LineDecoration[]; public readonly tabSize: number; + public readonly startVisibleColumn: number; public readonly spaceWidth: number; public readonly stopRenderingLineAfter: number; public readonly renderWhitespace: RenderWhitespace; @@ -89,6 +90,7 @@ export class RenderLineInput { lineTokens: IViewLineTokens, lineDecorations: LineDecoration[], tabSize: number, + startVisibleColumn: number, spaceWidth: number, stopRenderingLineAfter: number, renderWhitespace: 'none' | 'boundary' | 'selection' | 'all', @@ -106,6 +108,7 @@ export class RenderLineInput { this.lineTokens = lineTokens; this.lineDecorations = lineDecorations; this.tabSize = tabSize; + this.startVisibleColumn = startVisibleColumn; this.spaceWidth = spaceWidth; this.stopRenderingLineAfter = stopRenderingLineAfter; this.renderWhitespace = ( @@ -154,6 +157,7 @@ export class RenderLineInput { && this.containsRTL === other.containsRTL && this.fauxIndentLength === other.fauxIndentLength && this.tabSize === other.tabSize + && this.startVisibleColumn === other.startVisibleColumn && this.spaceWidth === other.spaceWidth && this.stopRenderingLineAfter === other.stopRenderingLineAfter && this.renderWhitespace === other.renderWhitespace @@ -368,7 +372,9 @@ class ResolvedRenderLineInput { public readonly isOverflowing: boolean, public readonly parts: LinePart[], public readonly containsForeignElements: ForeignElementType, + public readonly fauxIndentLength: number, public readonly tabSize: number, + public readonly startVisibleColumn: number, public readonly containsRTL: boolean, public readonly spaceWidth: number, public readonly renderWhitespace: RenderWhitespace, @@ -395,7 +401,7 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput let tokens = transformAndRemoveOverflowing(input.lineTokens, input.fauxIndentLength, len); 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); + tokens = _applyRenderWhitespace(lineContent, len, input.continuesWithWrappedLine, tokens, input.fauxIndentLength, input.tabSize, input.startVisibleColumn, useMonospaceOptimizations, input.selectionsOnLine, input.renderWhitespace === RenderWhitespace.Boundary); } let containsForeignElements = ForeignElementType.None; if (input.lineDecorations.length > 0) { @@ -425,7 +431,9 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput isOverflowing, tokens, containsForeignElements, + input.fauxIndentLength, input.tabSize, + input.startVisibleColumn, input.containsRTL, input.spaceWidth, input.renderWhitespace, @@ -537,7 +545,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 (→ or ·) do not have the same width as  . * 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, selections: LineRange[] | null, onlyBoundary: boolean): LinePart[] { +function _applyRenderWhitespace(lineContent: string, len: number, continuesWithWrappedLine: boolean, tokens: LinePart[], fauxIndentLength: number, tabSize: number, startVisibleColumn: number, useMonospaceOptimizations: boolean, selections: LineRange[] | null, onlyBoundary: boolean): LinePart[] { let result: LinePart[] = [], resultLen = 0; let tokenIndex = 0; @@ -555,21 +563,10 @@ function _applyRenderWhitespace(lineContent: string, len: number, continuesWithW lastNonWhitespaceIndex = strings.lastNonWhitespaceIndex(lineContent); } - let tmpIndent = 0; - for (let charIndex = 0; charIndex < fauxIndentLength; charIndex++) { - const chCode = lineContent.charCodeAt(charIndex); - if (chCode === CharCode.Tab) { - tmpIndent = tabSize; - } else if (strings.isFullWidthCharacter(chCode)) { - tmpIndent += 2; - } else { - tmpIndent++; - } - } - tmpIndent = tmpIndent % tabSize; let wasInWhitespace = false; let currentSelectionIndex = 0; let currentSelection = selections && selections[currentSelectionIndex]; + let tmpIndent = startVisibleColumn % tabSize; for (let charIndex = fauxIndentLength; charIndex < len; charIndex++) { const chCode = lineContent.charCodeAt(charIndex); @@ -729,7 +726,9 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render const len = input.len; const isOverflowing = input.isOverflowing; const parts = input.parts; + const fauxIndentLength = input.fauxIndentLength; const tabSize = input.tabSize; + const startVisibleColumn = input.startVisibleColumn; const containsRTL = input.containsRTL; const spaceWidth = input.spaceWidth; const renderWhitespace = input.renderWhitespace; @@ -738,7 +737,7 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render const characterMapping = new CharacterMapping(len + 1, parts.length); let charIndex = 0; - let tabsCharDelta = 0; + let visibleColumn = startVisibleColumn; let charOffsetInPart = 0; let prevPartContentCnt = 0; @@ -764,18 +763,14 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render let partContentCnt = 0; { let _charIndex = charIndex; - let _tabsCharDelta = tabsCharDelta; + let _visibleColumn = visibleColumn; for (; _charIndex < partEndIndex; _charIndex++) { const charCode = lineContent.charCodeAt(_charIndex); - - if (charCode === CharCode.Tab) { - let insertSpacesCount = tabSize - (_charIndex + _tabsCharDelta) % tabSize; - _tabsCharDelta += insertSpacesCount - 1; - partContentCnt += insertSpacesCount; - } else { - // must be CharCode.Space - partContentCnt++; + const charWidth = (charCode === CharCode.Tab ? (tabSize - (_visibleColumn % tabSize)) : 1) | 0; + partContentCnt += charWidth; + if (_charIndex >= fauxIndentLength) { + _visibleColumn += charWidth; } } } @@ -793,29 +788,30 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render for (; charIndex < partEndIndex; charIndex++) { characterMapping.setPartData(charIndex, partIndex, charOffsetInPart, partAbsoluteOffset); const charCode = lineContent.charCodeAt(charIndex); + let charWidth: number; if (charCode === CharCode.Tab) { - let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize; - tabsCharDelta += insertSpacesCount - 1; - charOffsetInPart += insertSpacesCount - 1; - if (insertSpacesCount > 0) { - if (!canUseHalfwidthRightwardsArrow || insertSpacesCount > 1) { - sb.write1(0x2192); // RIGHTWARDS ARROW - } else { - sb.write1(0xFFEB); // HALFWIDTH RIGHTWARDS ARROW - } - insertSpacesCount--; + charWidth = (tabSize - (visibleColumn % tabSize)) | 0; + + if (!canUseHalfwidthRightwardsArrow || charWidth > 1) { + sb.write1(0x2192); // RIGHTWARDS ARROW + } else { + sb.write1(0xFFEB); // HALFWIDTH RIGHTWARDS ARROW } - while (insertSpacesCount > 0) { + for (let space = 2; space <= charWidth; space++) { sb.write1(0xA0); //   - insertSpacesCount--; } - } else { - // must be CharCode.Space + + } else { // must be CharCode.Space + charWidth = 1; + sb.write1(0xB7); // · } - charOffsetInPart++; + charOffsetInPart += charWidth; + if (charIndex >= fauxIndentLength) { + visibleColumn += charWidth; + } } prevPartContentCnt = partContentCnt; @@ -833,63 +829,59 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render characterMapping.setPartData(charIndex, partIndex, charOffsetInPart, partAbsoluteOffset); const charCode = lineContent.charCodeAt(charIndex); + let producedCharacters = 1; + let charWidth = 1; + switch (charCode) { case CharCode.Tab: - let insertSpacesCount = tabSize - (charIndex + tabsCharDelta) % tabSize; - tabsCharDelta += insertSpacesCount - 1; - charOffsetInPart += insertSpacesCount - 1; - while (insertSpacesCount > 0) { + producedCharacters = (tabSize - (visibleColumn % tabSize)); + charWidth = producedCharacters; + for (let space = 1; space <= producedCharacters; space++) { sb.write1(0xA0); //   - partContentCnt++; - insertSpacesCount--; } break; case CharCode.Space: sb.write1(0xA0); //   - partContentCnt++; break; case CharCode.LessThan: sb.appendASCIIString('<'); - partContentCnt++; break; case CharCode.GreaterThan: sb.appendASCIIString('>'); - partContentCnt++; break; case CharCode.Ampersand: sb.appendASCIIString('&'); - partContentCnt++; break; case CharCode.Null: sb.appendASCIIString('�'); - partContentCnt++; break; case CharCode.UTF8_BOM: case CharCode.LINE_SEPARATOR_2028: sb.write1(0xFFFD); - partContentCnt++; break; default: if (strings.isFullWidthCharacter(charCode)) { - tabsCharDelta++; + charWidth++; } if (renderControlCharacters && charCode < 32) { sb.write1(9216 + charCode); - partContentCnt++; } else { sb.write1(charCode); - partContentCnt++; } } - charOffsetInPart++; + charOffsetInPart += producedCharacters; + partContentCnt += producedCharacters; + if (charIndex >= fauxIndentLength) { + visibleColumn += charWidth; + } } prevPartContentCnt = partContentCnt; diff --git a/src/vs/editor/common/viewModel/characterHardWrappingLineMapper.ts b/src/vs/editor/common/viewModel/characterHardWrappingLineMapper.ts deleted file mode 100644 index bbf71e817b93f4d9c6da41e2cfa5f200a6af3e35..0000000000000000000000000000000000000000 --- a/src/vs/editor/common/viewModel/characterHardWrappingLineMapper.ts +++ /dev/null @@ -1,293 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CharCode } from 'vs/base/common/charCode'; -import * as strings from 'vs/base/common/strings'; -import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; -import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier'; -import { toUint32Array } from 'vs/base/common/uint'; -import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { ILineMapperFactory, ILineMapping, OutputPosition } from 'vs/editor/common/viewModel/splitLinesCollection'; - -const enum CharacterClass { - NONE = 0, - BREAK_BEFORE = 1, - BREAK_AFTER = 2, - BREAK_OBTRUSIVE = 3, - BREAK_IDEOGRAPHIC = 4 // for Han and Kana. -} - -class WrappingCharacterClassifier extends CharacterClassifier { - - constructor(BREAK_BEFORE: string, BREAK_AFTER: string, BREAK_OBTRUSIVE: string) { - super(CharacterClass.NONE); - - for (let i = 0; i < BREAK_BEFORE.length; i++) { - this.set(BREAK_BEFORE.charCodeAt(i), CharacterClass.BREAK_BEFORE); - } - - for (let i = 0; i < BREAK_AFTER.length; i++) { - this.set(BREAK_AFTER.charCodeAt(i), CharacterClass.BREAK_AFTER); - } - - for (let i = 0; i < BREAK_OBTRUSIVE.length; i++) { - this.set(BREAK_OBTRUSIVE.charCodeAt(i), CharacterClass.BREAK_OBTRUSIVE); - } - } - - public get(charCode: number): CharacterClass { - // Initialize CharacterClass.BREAK_IDEOGRAPHIC for these Unicode ranges: - // 1. CJK Unified Ideographs (0x4E00 -- 0x9FFF) - // 2. CJK Unified Ideographs Extension A (0x3400 -- 0x4DBF) - // 3. Hiragana and Katakana (0x3040 -- 0x30FF) - if ( - (charCode >= 0x3040 && charCode <= 0x30FF) - || (charCode >= 0x3400 && charCode <= 0x4DBF) - || (charCode >= 0x4E00 && charCode <= 0x9FFF) - ) { - return CharacterClass.BREAK_IDEOGRAPHIC; - } - - return super.get(charCode); - } -} - -export class CharacterHardWrappingLineMapperFactory implements ILineMapperFactory { - - private readonly classifier: WrappingCharacterClassifier; - - constructor(breakBeforeChars: string, breakAfterChars: string, breakObtrusiveChars: string) { - this.classifier = new WrappingCharacterClassifier(breakBeforeChars, breakAfterChars, breakObtrusiveChars); - } - - // TODO@Alex -> duplicated in lineCommentCommand - private static nextVisibleColumn(currentVisibleColumn: number, tabSize: number, isTab: boolean, columnSize: number): number { - currentVisibleColumn = +currentVisibleColumn; //@perf - tabSize = +tabSize; //@perf - columnSize = +columnSize; //@perf - - if (isTab) { - return currentVisibleColumn + (tabSize - (currentVisibleColumn % tabSize)); - } - return currentVisibleColumn + columnSize; - } - - public createLineMapping(lineText: string, tabSize: number, breakingColumn: number, columnsForFullWidthChar: number, hardWrappingIndent: WrappingIndent): ILineMapping | null { - if (breakingColumn === -1) { - return null; - } - - tabSize = +tabSize; //@perf - breakingColumn = +breakingColumn; //@perf - columnsForFullWidthChar = +columnsForFullWidthChar; //@perf - hardWrappingIndent = +hardWrappingIndent; //@perf - - let wrappedTextIndentVisibleColumn = 0; - let wrappedTextIndent = ''; - - let firstNonWhitespaceIndex = -1; - if (hardWrappingIndent !== WrappingIndent.None) { - firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText); - if (firstNonWhitespaceIndex !== -1) { - // Track existing indent - wrappedTextIndent = lineText.substring(0, firstNonWhitespaceIndex); - for (let i = 0; i < firstNonWhitespaceIndex; i++) { - wrappedTextIndentVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(wrappedTextIndentVisibleColumn, tabSize, lineText.charCodeAt(i) === CharCode.Tab, 1); - } - - // Increase indent of continuation lines, if desired - let numberOfAdditionalTabs = 0; - if (hardWrappingIndent === WrappingIndent.Indent) { - numberOfAdditionalTabs = 1; - } else if (hardWrappingIndent === WrappingIndent.DeepIndent) { - numberOfAdditionalTabs = 2; - } - for (let i = 0; i < numberOfAdditionalTabs; i++) { - wrappedTextIndent += '\t'; - wrappedTextIndentVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(wrappedTextIndentVisibleColumn, tabSize, true, 1); - } - - // Force sticking to beginning of line if no character would fit except for the indentation - if (wrappedTextIndentVisibleColumn + columnsForFullWidthChar > breakingColumn) { - wrappedTextIndent = ''; - wrappedTextIndentVisibleColumn = 0; - } - } - } - - let classifier = this.classifier; - let lastBreakingOffset = 0; // Last 0-based offset in the lineText at which a break happened - let breakingLengths: number[] = []; // The length of each broken-up line text - let breakingLengthsIndex: number = 0; // The count of breaks already done - let visibleColumn = 0; // Visible column since the beginning of the current line - let niceBreakOffset = -1; // Last index of a character that indicates a break should happen before it (more desirable) - let niceBreakVisibleColumn = 0; // visible column if a break were to be later introduced before `niceBreakOffset` - let obtrusiveBreakOffset = -1; // Last index of a character that indicates a break should happen before it (less desirable) - let obtrusiveBreakVisibleColumn = 0; // visible column if a break were to be later introduced before `obtrusiveBreakOffset` - let len = lineText.length; - - for (let i = 0; i < len; i++) { - // At this point, there is a certainty that the character before `i` fits on the current line, - // but the character at `i` might not fit - - let charCode = lineText.charCodeAt(i); - let charCodeIsTab = (charCode === CharCode.Tab); - let charCodeClass = classifier.get(charCode); - - if (strings.isLowSurrogate(charCode)/* && i + 1 < len */) { - // A surrogate pair must always be considered as a single unit, so it is never to be broken - // => advance visibleColumn by 1 and advance to next char code... - visibleColumn = visibleColumn + 1; - continue; - } - - if (charCodeClass === CharacterClass.BREAK_BEFORE) { - // This is a character that indicates that a break should happen before it - // Since we are certain the character before `i` fits, there's no extra checking needed, - // just mark it as a nice breaking opportunity - niceBreakOffset = i; - niceBreakVisibleColumn = wrappedTextIndentVisibleColumn; - } - - // CJK breaking : before break - if (charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && i > 0) { - let prevCode = lineText.charCodeAt(i - 1); - let prevClass = classifier.get(prevCode); - if (prevClass !== CharacterClass.BREAK_BEFORE) { // Kinsoku Shori: Don't break after a leading character, like an open bracket - niceBreakOffset = i; - niceBreakVisibleColumn = wrappedTextIndentVisibleColumn; - } - } - - let charColumnSize = 1; - if (strings.isFullWidthCharacter(charCode)) { - charColumnSize = columnsForFullWidthChar; - } - - // Advance visibleColumn with character at `i` - visibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(visibleColumn, tabSize, charCodeIsTab, charColumnSize); - - if (visibleColumn > breakingColumn && i !== 0) { - // We need to break at least before character at `i`: - // - break before niceBreakLastOffset if it exists (and re-establish a correct visibleColumn by using niceBreakVisibleColumn + charAt(i)) - // - otherwise, break before obtrusiveBreakLastOffset if it exists (and re-establish a correct visibleColumn by using obtrusiveBreakVisibleColumn + charAt(i)) - // - otherwise, break before i (and re-establish a correct visibleColumn by charAt(i)) - - let breakBeforeOffset: number; - let restoreVisibleColumnFrom: number; - - if (niceBreakOffset !== -1 && niceBreakVisibleColumn <= breakingColumn) { - - // We will break before `niceBreakLastOffset` - breakBeforeOffset = niceBreakOffset; - restoreVisibleColumnFrom = niceBreakVisibleColumn; - - } else if (obtrusiveBreakOffset !== -1 && obtrusiveBreakVisibleColumn <= breakingColumn) { - - // We will break before `obtrusiveBreakLastOffset` - breakBeforeOffset = obtrusiveBreakOffset; - restoreVisibleColumnFrom = obtrusiveBreakVisibleColumn; - - } else { - - // We will break before `i` - breakBeforeOffset = i; - restoreVisibleColumnFrom = wrappedTextIndentVisibleColumn; - - } - - // Break before character at `breakBeforeOffset` - breakingLengths[breakingLengthsIndex++] = breakBeforeOffset - lastBreakingOffset; - lastBreakingOffset = breakBeforeOffset; - - // Re-establish visibleColumn by taking character at `i` into account - visibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(restoreVisibleColumnFrom, tabSize, charCodeIsTab, charColumnSize); - - // Reset markers - niceBreakOffset = -1; - niceBreakVisibleColumn = 0; - obtrusiveBreakOffset = -1; - obtrusiveBreakVisibleColumn = 0; - } - - // At this point, there is a certainty that the character at `i` fits on the current line - - if (niceBreakOffset !== -1) { - // Advance niceBreakVisibleColumn - niceBreakVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(niceBreakVisibleColumn, tabSize, charCodeIsTab, charColumnSize); - } - if (obtrusiveBreakOffset !== -1) { - // Advance obtrusiveBreakVisibleColumn - obtrusiveBreakVisibleColumn = CharacterHardWrappingLineMapperFactory.nextVisibleColumn(obtrusiveBreakVisibleColumn, tabSize, charCodeIsTab, charColumnSize); - } - - if (charCodeClass === CharacterClass.BREAK_AFTER && (hardWrappingIndent === WrappingIndent.None || i >= firstNonWhitespaceIndex)) { - // This is a character that indicates that a break should happen after it - niceBreakOffset = i + 1; - niceBreakVisibleColumn = wrappedTextIndentVisibleColumn; - } - - // CJK breaking : after break - if (charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && i < len - 1) { - let nextCode = lineText.charCodeAt(i + 1); - let nextClass = classifier.get(nextCode); - if (nextClass !== CharacterClass.BREAK_AFTER) { // Kinsoku Shori: Don't break before a trailing character, like a period - niceBreakOffset = i + 1; - niceBreakVisibleColumn = wrappedTextIndentVisibleColumn; - } - } - - if (charCodeClass === CharacterClass.BREAK_OBTRUSIVE) { - // This is an obtrusive character that indicates that a break should happen after it - obtrusiveBreakOffset = i + 1; - obtrusiveBreakVisibleColumn = wrappedTextIndentVisibleColumn; - } - } - - if (breakingLengthsIndex === 0) { - return null; - } - - // Add last segment - breakingLengths[breakingLengthsIndex++] = len - lastBreakingOffset; - - return new CharacterHardWrappingLineMapping( - new PrefixSumComputer(toUint32Array(breakingLengths)), - wrappedTextIndent - ); - } -} - -export class CharacterHardWrappingLineMapping implements ILineMapping { - - private readonly _prefixSums: PrefixSumComputer; - private readonly _wrappedLinesIndent: string; - - constructor(prefixSums: PrefixSumComputer, wrappedLinesIndent: string) { - this._prefixSums = prefixSums; - this._wrappedLinesIndent = wrappedLinesIndent; - } - - public getOutputLineCount(): number { - return this._prefixSums.getCount(); - } - - public getWrappedLinesIndent(): string { - return this._wrappedLinesIndent; - } - - public getInputOffsetOfOutputPosition(outputLineIndex: number, outputOffset: number): number { - if (outputLineIndex === 0) { - return outputOffset; - } else { - return this._prefixSums.getAccumulatedValue(outputLineIndex - 1) + outputOffset; - } - } - - public getOutputPositionOfInputOffset(inputOffset: number): OutputPosition { - let r = this._prefixSums.getIndexOf(inputOffset); - return new OutputPosition(r.index, r.remainder); - } -} diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts new file mode 100644 index 0000000000000000000000000000000000000000..be52ca2265a7f160443a267a123c3835577f7fb7 --- /dev/null +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -0,0 +1,465 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CharCode } from 'vs/base/common/charCode'; +import * as strings from 'vs/base/common/strings'; +import { WrappingIndent, IComputedEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { CharacterClassifier } from 'vs/editor/common/core/characterClassifier'; +import { ILineBreaksComputerFactory, LineBreakData, ILineBreaksComputer } from 'vs/editor/common/viewModel/splitLinesCollection'; + +const enum CharacterClass { + NONE = 0, + BREAK_BEFORE = 1, + BREAK_AFTER = 2, + BREAK_IDEOGRAPHIC = 3 // for Han and Kana. +} + +class WrappingCharacterClassifier extends CharacterClassifier { + + constructor(BREAK_BEFORE: string, BREAK_AFTER: string) { + super(CharacterClass.NONE); + + for (let i = 0; i < BREAK_BEFORE.length; i++) { + this.set(BREAK_BEFORE.charCodeAt(i), CharacterClass.BREAK_BEFORE); + } + + for (let i = 0; i < BREAK_AFTER.length; i++) { + this.set(BREAK_AFTER.charCodeAt(i), CharacterClass.BREAK_AFTER); + } + } + + public get(charCode: number): CharacterClass { + if (charCode >= 0 && charCode < 256) { + return this._asciiMap[charCode]; + } else { + // Initialize CharacterClass.BREAK_IDEOGRAPHIC for these Unicode ranges: + // 1. CJK Unified Ideographs (0x4E00 -- 0x9FFF) + // 2. CJK Unified Ideographs Extension A (0x3400 -- 0x4DBF) + // 3. Hiragana and Katakana (0x3040 -- 0x30FF) + if ( + (charCode >= 0x3040 && charCode <= 0x30FF) + || (charCode >= 0x3400 && charCode <= 0x4DBF) + || (charCode >= 0x4E00 && charCode <= 0x9FFF) + ) { + return CharacterClass.BREAK_IDEOGRAPHIC; + } + + return (this._map.get(charCode) || this._defaultValue); + } + } +} + +export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFactory { + + public static create(options: IComputedEditorOptions): MonospaceLineBreaksComputerFactory { + return new MonospaceLineBreaksComputerFactory( + options.get(EditorOption.wordWrapBreakBeforeCharacters), + options.get(EditorOption.wordWrapBreakAfterCharacters) + ); + } + + private readonly classifier: WrappingCharacterClassifier; + + constructor(breakBeforeChars: string, breakAfterChars: string) { + this.classifier = new WrappingCharacterClassifier(breakBeforeChars, breakAfterChars); + } + + public createLineBreaksComputer(tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): ILineBreaksComputer { + tabSize = tabSize | 0; //@perf + wrappingColumn = +wrappingColumn; //@perf + columnsForFullWidthChar = +columnsForFullWidthChar; //@perf + + let requests: string[] = []; + let previousBreakingData: (LineBreakData | null)[] = []; + return { + addRequest: (lineText: string, previousLineBreakData: LineBreakData | null) => { + requests.push(lineText); + previousBreakingData.push(previousLineBreakData); + }, + finalize: () => { + let result: (LineBreakData | null)[] = []; + for (let i = 0, len = requests.length; i < len; i++) { + const previousLineBreakData = previousBreakingData[i]; + if (previousLineBreakData) { + result[i] = createLineBreaksFromPreviousLineBreaks(this.classifier, previousLineBreakData, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent); + } else { + result[i] = createLineBreaks(this.classifier, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent); + } + } + return result; + } + }; + } +} + +let arrPool1: number[] = []; +let arrPool2: number[] = []; +function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterClassifier, previousBreakingData: LineBreakData, lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): LineBreakData | null { + if (firstLineBreakColumn === -1) { + return null; + } + + const len = lineText.length; + if (len <= 1) { + return null; + } + + const prevBreakingOffsets = previousBreakingData.breakOffsets; + const prevBreakingOffsetsVisibleColumn = previousBreakingData.breakOffsetsVisibleColumn; + + const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent); + const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength; + + let breakingOffsets: number[] = arrPool1; + let breakingOffsetsVisibleColumn: number[] = arrPool2; + let breakingOffsetsCount: number = 0; + + let breakingColumn = firstLineBreakColumn; + const prevLen = prevBreakingOffsets.length; + let prevIndex = 0; + + if (prevIndex >= 0) { + let bestDistance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex] - breakingColumn); + while (prevIndex + 1 < prevLen) { + const distance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex + 1] - breakingColumn); + if (distance >= bestDistance) { + break; + } + bestDistance = distance; + prevIndex++; + } + } + + while (prevIndex < prevLen) { + // Allow for prevIndex to be -1 (for the case where we hit a tab when walking backwards from the first break) + const prevBreakOffset = prevIndex < 0 ? 0 : prevBreakingOffsets[prevIndex]; + const prevBreakoffsetVisibleColumn = prevIndex < 0 ? 0 : prevBreakingOffsetsVisibleColumn[prevIndex]; + + let breakOffset = 0; + let breakOffsetVisibleColumn = 0; + + let forcedBreakOffset = 0; + let forcedBreakOffsetVisibleColumn = 0; + + // initially, we search as much as possible to the right (if it fits) + if (prevBreakoffsetVisibleColumn <= breakingColumn) { + let visibleColumn = prevBreakoffsetVisibleColumn; + let prevCharCode = lineText.charCodeAt(prevBreakOffset - 1); + let prevCharCodeClass = classifier.get(prevCharCode); + let entireLineFits = true; + for (let i = prevBreakOffset; i < len; i++) { + const charStartOffset = i; + const charCode = lineText.charCodeAt(i); + let charCodeClass: number; + let charWidth: number; + + if (strings.isHighSurrogate(charCode)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + i++; + charCodeClass = CharacterClass.NONE; + charWidth = 2; + } else { + charCodeClass = classifier.get(charCode); + charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar); + } + + if (canBreak(prevCharCodeClass, charCodeClass)) { + breakOffset = charStartOffset; + breakOffsetVisibleColumn = visibleColumn; + } + + visibleColumn += charWidth; + + // check if adding character at `i` will go over the breaking column + if (visibleColumn > breakingColumn) { + // We need to break at least before character at `i`: + forcedBreakOffset = charStartOffset; + forcedBreakOffsetVisibleColumn = visibleColumn - charWidth; + + if (visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) { + // Cannot break at `breakOffset` => reset it if it was set + breakOffset = 0; + } + + entireLineFits = false; + break; + } + + prevCharCode = charCode; + prevCharCodeClass = charCodeClass; + } + + if (entireLineFits) { + // there is no more need to break => stop the outer loop! + if (breakingOffsetsCount > 0) { + // Add last segment + breakingOffsets[breakingOffsetsCount] = prevBreakingOffsets[prevBreakingOffsets.length - 1]; + breakingOffsetsVisibleColumn[breakingOffsetsCount] = prevBreakingOffsetsVisibleColumn[prevBreakingOffsets.length - 1]; + breakingOffsetsCount++; + } + break; + } + } + + if (breakOffset === 0) { + // must search left + let visibleColumn = prevBreakoffsetVisibleColumn; + let charCode = lineText.charCodeAt(prevBreakOffset); + let charCodeClass = classifier.get(charCode); + let hitATabCharacter = false; + for (let i = prevBreakOffset - 1; i >= 0; i--) { + const charStartOffset = i + 1; + const prevCharCode = lineText.charCodeAt(i); + + if (prevCharCode === CharCode.Tab) { + // cannot determine the width of a tab when going backwards, so we must go forwards + hitATabCharacter = true; + break; + } + + let prevCharCodeClass: number; + let prevCharWidth: number; + + if (strings.isLowSurrogate(prevCharCode)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + i--; + prevCharCodeClass = CharacterClass.NONE; + prevCharWidth = 2; + } else { + prevCharCodeClass = classifier.get(prevCharCode); + prevCharWidth = (strings.isFullWidthCharacter(prevCharCode) ? columnsForFullWidthChar : 1); + } + + if (visibleColumn <= breakingColumn) { + if (forcedBreakOffset === 0) { + forcedBreakOffset = charStartOffset; + forcedBreakOffsetVisibleColumn = visibleColumn; + } + + if (visibleColumn <= breakingColumn - wrappedLineBreakColumn) { + // went too far! + break; + } + + if (canBreak(prevCharCodeClass, charCodeClass)) { + breakOffset = charStartOffset; + breakOffsetVisibleColumn = visibleColumn; + break; + } + } + + visibleColumn -= prevCharWidth; + charCode = prevCharCode; + charCodeClass = prevCharCodeClass; + } + + if (breakOffset !== 0) { + const remainingWidthOfNextLine = wrappedLineBreakColumn - (forcedBreakOffsetVisibleColumn - breakOffsetVisibleColumn); + if (remainingWidthOfNextLine <= tabSize) { + const charCodeAtForcedBreakOffset = lineText.charCodeAt(forcedBreakOffset); + let charWidth: number; + if (strings.isHighSurrogate(charCodeAtForcedBreakOffset)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + charWidth = 2; + } else { + charWidth = computeCharWidth(charCodeAtForcedBreakOffset, forcedBreakOffsetVisibleColumn, tabSize, columnsForFullWidthChar); + } + if (remainingWidthOfNextLine - charWidth < 0) { + // it is not worth it to break at breakOffset, it just introduces an extra needless line! + breakOffset = 0; + } + } + } + + if (hitATabCharacter) { + // cannot determine the width of a tab when going backwards, so we must go forwards from the previous break + prevIndex--; + continue; + } + } + + if (breakOffset === 0) { + // Could not find a good breaking point + breakOffset = forcedBreakOffset; + breakOffsetVisibleColumn = forcedBreakOffsetVisibleColumn; + } + + breakingOffsets[breakingOffsetsCount] = breakOffset; + breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; + breakingOffsetsCount++; + breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn; + + while (prevIndex < 0 || (prevIndex < prevLen && prevBreakingOffsetsVisibleColumn[prevIndex] < breakOffsetVisibleColumn)) { + prevIndex++; + } + + let bestDistance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex] - breakingColumn); + while (prevIndex + 1 < prevLen) { + const distance = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex + 1] - breakingColumn); + if (distance >= bestDistance) { + break; + } + bestDistance = distance; + prevIndex++; + } + } + + if (breakingOffsetsCount === 0) { + return null; + } + + // Doing here some object reuse which ends up helping a huge deal with GC pauses! + breakingOffsets.length = breakingOffsetsCount; + breakingOffsetsVisibleColumn.length = breakingOffsetsCount; + arrPool1 = previousBreakingData.breakOffsets; + arrPool2 = previousBreakingData.breakOffsetsVisibleColumn; + previousBreakingData.breakOffsets = breakingOffsets; + previousBreakingData.breakOffsetsVisibleColumn = breakingOffsetsVisibleColumn; + previousBreakingData.wrappedTextIndentLength = wrappedTextIndentLength; + return previousBreakingData; +} + +function createLineBreaks(classifier: WrappingCharacterClassifier, lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): LineBreakData | null { + if (firstLineBreakColumn === -1) { + return null; + } + + const len = lineText.length; + if (len <= 1) { + return null; + } + + const wrappedTextIndentLength = computeWrappedTextIndentLength(lineText, tabSize, firstLineBreakColumn, columnsForFullWidthChar, wrappingIndent); + const wrappedLineBreakColumn = firstLineBreakColumn - wrappedTextIndentLength; + + let breakingOffsets: number[] = []; + let breakingOffsetsVisibleColumn: number[] = []; + let breakingOffsetsCount: number = 0; + let breakOffset = 0; + let breakOffsetVisibleColumn = 0; + + let breakingColumn = firstLineBreakColumn; + let prevCharCode = lineText.charCodeAt(0); + let prevCharCodeClass = classifier.get(prevCharCode); + let visibleColumn = computeCharWidth(prevCharCode, 0, tabSize, columnsForFullWidthChar); + + let startOffset = 1; + if (strings.isHighSurrogate(prevCharCode)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + visibleColumn += 1; + prevCharCode = lineText.charCodeAt(1); + prevCharCodeClass = classifier.get(prevCharCode); + startOffset++; + } + + for (let i = startOffset; i < len; i++) { + const charStartOffset = i; + const charCode = lineText.charCodeAt(i); + let charCodeClass: number; + let charWidth: number; + + if (strings.isHighSurrogate(charCode)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + i++; + charCodeClass = CharacterClass.NONE; + charWidth = 2; + } else { + charCodeClass = classifier.get(charCode); + charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar); + } + + if (canBreak(prevCharCodeClass, charCodeClass)) { + breakOffset = charStartOffset; + breakOffsetVisibleColumn = visibleColumn; + } + + visibleColumn += charWidth; + + // check if adding character at `i` will go over the breaking column + if (visibleColumn > breakingColumn) { + // We need to break at least before character at `i`: + + if (breakOffset === 0 || visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakColumn) { + // Cannot break at `breakOffset`, must break at `i` + breakOffset = charStartOffset; + breakOffsetVisibleColumn = visibleColumn - charWidth; + } + + breakingOffsets[breakingOffsetsCount] = breakOffset; + breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; + breakingOffsetsCount++; + breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakColumn; + breakOffset = 0; + } + + prevCharCode = charCode; + prevCharCodeClass = charCodeClass; + } + + if (breakingOffsetsCount === 0) { + return null; + } + + // Add last segment + breakingOffsets[breakingOffsetsCount] = len; + breakingOffsetsVisibleColumn[breakingOffsetsCount] = visibleColumn; + + return new LineBreakData(breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength); +} + +function computeCharWidth(charCode: number, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number { + if (charCode === CharCode.Tab) { + return (tabSize - (visibleColumn % tabSize)); + } + if (strings.isFullWidthCharacter(charCode)) { + return columnsForFullWidthChar; + } + return 1; +} + +function tabCharacterWidth(visibleColumn: number, tabSize: number): number { + return (tabSize - (visibleColumn % tabSize)); +} + +/** + * Kinsoku Shori : Don't break after a leading character, like an open bracket + * Kinsoku Shori : Don't break before a trailing character, like a period + */ +function canBreak(prevCharCodeClass: CharacterClass, charCodeClass: CharacterClass): boolean { + return ( + (prevCharCodeClass === CharacterClass.BREAK_AFTER) + || (prevCharCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && charCodeClass !== CharacterClass.BREAK_AFTER) + || (charCodeClass === CharacterClass.BREAK_BEFORE) + || (charCodeClass === CharacterClass.BREAK_IDEOGRAPHIC && prevCharCodeClass !== CharacterClass.BREAK_BEFORE) + ); +} + +function computeWrappedTextIndentLength(lineText: string, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): number { + let wrappedTextIndentLength = 0; + if (wrappingIndent !== WrappingIndent.None) { + const firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineText); + if (firstNonWhitespaceIndex !== -1) { + // Track existing indent + + for (let i = 0; i < firstNonWhitespaceIndex; i++) { + const charWidth = (lineText.charCodeAt(i) === CharCode.Tab ? tabCharacterWidth(wrappedTextIndentLength, tabSize) : 1); + wrappedTextIndentLength += charWidth; + } + + // Increase indent of continuation lines, if desired + const numberOfAdditionalTabs = (wrappingIndent === WrappingIndent.DeepIndent ? 2 : wrappingIndent === WrappingIndent.Indent ? 1 : 0); + for (let i = 0; i < numberOfAdditionalTabs; i++) { + const charWidth = tabCharacterWidth(wrappedTextIndentLength, tabSize); + wrappedTextIndentLength += charWidth; + } + + // Force sticking to beginning of line if no character would fit except for the indentation + if (wrappedTextIndentLength + columnsForFullWidthChar > firstLineBreakColumn) { + wrappedTextIndentLength = 0; + } + } + } + return wrappedTextIndentLength; +} diff --git a/src/vs/editor/common/viewModel/prefixSumComputer.ts b/src/vs/editor/common/viewModel/prefixSumComputer.ts index d4e8c1f0df259b15c4a7ed17ac3135bd4d4e535d..6cb1312c07e2d207f4453006efeafcb1b7fa1456 100644 --- a/src/vs/editor/common/viewModel/prefixSumComputer.ts +++ b/src/vs/editor/common/viewModel/prefixSumComputer.ts @@ -187,73 +187,3 @@ export class PrefixSumComputer { return new PrefixSumIndexOfResult(mid, accumulatedValue - midStart); } } - -export class PrefixSumComputerWithCache { - - private readonly _actual: PrefixSumComputer; - private _cacheAccumulatedValueStart: number = 0; - private _cache: PrefixSumIndexOfResult[] | null = null; - - constructor(values: Uint32Array) { - this._actual = new PrefixSumComputer(values); - this._bustCache(); - } - - private _bustCache(): void { - this._cacheAccumulatedValueStart = 0; - this._cache = null; - } - - public insertValues(insertIndex: number, insertValues: Uint32Array): void { - if (this._actual.insertValues(insertIndex, insertValues)) { - this._bustCache(); - } - } - - public changeValue(index: number, value: number): void { - if (this._actual.changeValue(index, value)) { - this._bustCache(); - } - } - - public removeValues(startIndex: number, cnt: number): void { - if (this._actual.removeValues(startIndex, cnt)) { - this._bustCache(); - } - } - - public getTotalValue(): number { - return this._actual.getTotalValue(); - } - - public getAccumulatedValue(index: number): number { - return this._actual.getAccumulatedValue(index); - } - - public getIndexOf(accumulatedValue: number): PrefixSumIndexOfResult { - accumulatedValue = Math.floor(accumulatedValue); //@perf - - if (this._cache !== null) { - let cacheIndex = accumulatedValue - this._cacheAccumulatedValueStart; - if (cacheIndex >= 0 && cacheIndex < this._cache.length) { - // Cache hit! - return this._cache[cacheIndex]; - } - } - - // Cache miss! - return this._actual.getIndexOf(accumulatedValue); - } - - /** - * Gives a hint that a lot of requests are about to come in for these accumulated values. - */ - public warmUpCache(accumulatedValueStart: number, accumulatedValueEnd: number): void { - let newCache: PrefixSumIndexOfResult[] = []; - for (let accumulatedValue = accumulatedValueStart; accumulatedValue <= accumulatedValueEnd; accumulatedValue++) { - newCache[accumulatedValue - accumulatedValueStart] = this.getIndexOf(accumulatedValue); - } - this._cache = newCache; - this._cacheAccumulatedValueStart = accumulatedValueStart; - } -} diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 3cd2afbafaf85035399b13dfb540a97452f7de5a..726444f3b11e00a83de7929db8a9aa73e37fc226 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as arrays from 'vs/base/common/arrays'; import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Position } from 'vs/editor/common/core/position'; @@ -10,13 +11,12 @@ import { IRange, Range } from 'vs/editor/common/core/range'; import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecoration, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; import { ModelDecorationOptions, ModelDecorationOverviewRulerOptions } from 'vs/editor/common/model/textModel'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; -import { PrefixSumComputerWithCache } from 'vs/editor/common/viewModel/prefixSumComputer'; +import { PrefixSumIndexOfResult } from 'vs/editor/common/viewModel/prefixSumComputer'; import { ICoordinatesConverter, IOverviewRulerDecorations, ViewLineData } from 'vs/editor/common/viewModel/viewModel'; import { ITheme } from 'vs/platform/theme/common/themeService'; import { IDisposable } from 'vs/base/common/lifecycle'; export class OutputPosition { - _outputPositionBrand: void; outputLineIndex: number; outputOffset: number; @@ -26,15 +26,56 @@ export class OutputPosition { } } -export interface ILineMapping { - getOutputLineCount(): number; - getWrappedLinesIndent(): string; - getInputOffsetOfOutputPosition(outputLineIndex: number, outputOffset: number): number; - getOutputPositionOfInputOffset(inputOffset: number): OutputPosition; +export class LineBreakData { + constructor( + public breakOffsets: number[], + public breakOffsetsVisibleColumn: number[], + public wrappedTextIndentLength: number + ) { } + + public static getInputOffsetOfOutputPosition(breakOffsets: number[], outputLineIndex: number, outputOffset: number): number { + if (outputLineIndex === 0) { + return outputOffset; + } else { + return breakOffsets[outputLineIndex - 1] + outputOffset; + } + } + + public static getOutputPositionOfInputOffset(breakOffsets: number[], inputOffset: number): OutputPosition { + let low = 0; + let high = breakOffsets.length - 1; + let mid = 0; + let midStart = 0; + + while (low <= high) { + mid = low + ((high - low) / 2) | 0; + + const midStop = breakOffsets[mid]; + midStart = mid > 0 ? breakOffsets[mid - 1] : 0; + + if (inputOffset < midStart) { + high = mid - 1; + } else if (inputOffset >= midStop) { + low = mid + 1; + } else { + break; + } + } + + return new OutputPosition(mid, inputOffset - midStart); + } +} + +export interface ILineBreaksComputer { + /** + * Pass in `previousLineBreakData` if the only difference is in breaking columns!!! + */ + addRequest(lineText: string, previousLineBreakData: LineBreakData | null): void; + finalize(): (LineBreakData | null)[]; } -export interface ILineMapperFactory { - createLineMapping(lineText: string, tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): ILineMapping | null; +export interface ILineBreaksComputerFactory { + createLineBreaksComputer(tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent): ILineBreaksComputer; } export interface ISimpleModel { @@ -50,6 +91,7 @@ export interface ISplitLine { isVisible(): boolean; setVisible(isVisible: boolean): ISplitLine; + getLineBreakData(): LineBreakData | null; getViewLineCount(): number; getViewLineContent(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): string; getViewLineLength(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number; @@ -71,14 +113,14 @@ export interface IViewModelLinesCollection extends IDisposable { getHiddenAreas(): Range[]; setHiddenAreas(_ranges: Range[]): boolean; + createLineBreaksComputer(): ILineBreaksComputer; onModelFlushed(): void; onModelLinesDeleted(versionId: number, fromLineNumber: number, toLineNumber: number): viewEvents.ViewLinesDeletedEvent | null; - onModelLinesInserted(versionId: number, fromLineNumber: number, toLineNumber: number, text: string[]): viewEvents.ViewLinesInsertedEvent | null; - onModelLineChanged(versionId: number, lineNumber: number, newText: string): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null]; + onModelLinesInserted(versionId: number, fromLineNumber: number, toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null; + onModelLineChanged(versionId: number, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null]; acceptVersionId(versionId: number): void; getViewLineCount(): number; - warmUpLookupCache(viewStartLineNumber: number, viewEndLineNumber: number): void; getActiveIndentGuide(viewLineNumber: number, minLineNumber: number, maxLineNumber: number): IActiveIndentGuideInfo; getViewLinesIndentGuides(viewStartLineNumber: number, viewEndLineNumber: number): number[]; getViewLineContent(viewLineNumber: number): string; @@ -107,9 +149,7 @@ export class CoordinatesConverter implements ICoordinatesConverter { } public convertViewRangeToModelRange(viewRange: Range): Range { - let start = this._lines.convertViewPositionToModelPosition(viewRange.startLineNumber, viewRange.startColumn); - let end = this._lines.convertViewPositionToModelPosition(viewRange.endLineNumber, viewRange.endColumn); - return new Range(start.lineNumber, start.column, end.lineNumber, end.column); + return this._lines.convertViewRangeToModelRange(viewRange); } public validateViewPosition(viewPosition: Position, expectedModelPosition: Position): Position { @@ -117,9 +157,7 @@ export class CoordinatesConverter implements ICoordinatesConverter { } public validateViewRange(viewRange: Range, expectedModelRange: Range): Range { - const validViewStart = this._lines.validateViewPosition(viewRange.startLineNumber, viewRange.startColumn, expectedModelRange.getStartPosition()); - const validViewEnd = this._lines.validateViewPosition(viewRange.endLineNumber, viewRange.endColumn, expectedModelRange.getEndPosition()); - return new Range(validViewStart.lineNumber, validViewStart.column, validViewEnd.lineNumber, validViewEnd.column); + return this._lines.validateViewRange(viewRange, expectedModelRange); } // Model -> View conversion and related methods @@ -135,7 +173,6 @@ export class CoordinatesConverter implements ICoordinatesConverter { public modelPositionIsVisible(modelPosition: Position): boolean { return this._lines.modelPositionIsVisible(modelPosition.lineNumber, modelPosition.column); } - } const enum IndentGuideRepeatOption { @@ -144,6 +181,89 @@ const enum IndentGuideRepeatOption { BlockAll = 2 } +class LineNumberMapper { + + private _counts: number[]; + private _isValid: boolean; + private _validEndIndex: number; + + private _modelToView: number[]; + private _viewToModel: number[]; + + constructor(viewLineCounts: number[]) { + this._counts = viewLineCounts; + this._isValid = false; + this._validEndIndex = -1; + this._modelToView = []; + this._viewToModel = []; + } + + private _invalidate(index: number): void { + this._isValid = false; + this._validEndIndex = Math.min(this._validEndIndex, index - 1); + } + + private _ensureValid(): void { + if (this._isValid) { + return; + } + + for (let i = this._validEndIndex + 1, len = this._counts.length; i < len; i++) { + const viewLineCount = this._counts[i]; + const viewLinesAbove = (i > 0 ? this._modelToView[i - 1] : 0); + + this._modelToView[i] = viewLinesAbove + viewLineCount; + for (let j = 0; j < viewLineCount; j++) { + this._viewToModel[viewLinesAbove + j] = i; + } + } + + // trim things + this._modelToView.length = this._counts.length; + this._viewToModel.length = this._modelToView[this._modelToView.length - 1]; + + // mark as valid + this._isValid = true; + this._validEndIndex = this._counts.length - 1; + } + + public changeValue(index: number, value: number): void { + if (this._counts[index] === value) { + // no change + return; + } + this._counts[index] = value; + this._invalidate(index); + } + + public removeValues(start: number, deleteCount: number): void { + this._counts.splice(start, deleteCount); + this._invalidate(start); + } + + public insertValues(insertIndex: number, insertArr: number[]): void { + this._counts = arrays.arrayInsert(this._counts, insertIndex, insertArr); + this._invalidate(insertIndex); + } + + public getTotalValue(): number { + this._ensureValid(); + return this._viewToModel.length; + } + + public getAccumulatedValue(index: number): number { + this._ensureValid(); + return this._modelToView[index]; + } + + public getIndexOf(accumulatedValue: number): PrefixSumIndexOfResult { + this._ensureValid(); + const modelLineIndex = this._viewToModel[accumulatedValue]; + const viewLinesAbove = (modelLineIndex > 0 ? this._modelToView[modelLineIndex - 1] : 0); + return new PrefixSumIndexOfResult(modelLineIndex, accumulatedValue - viewLinesAbove); + } +} + export class SplitLinesCollection implements IViewModelLinesCollection { private readonly model: ITextModel; @@ -155,13 +275,13 @@ export class SplitLinesCollection implements IViewModelLinesCollection { private tabSize: number; private lines!: ISplitLine[]; - private prefixSumComputer!: PrefixSumComputerWithCache; + private prefixSumComputer!: LineNumberMapper; - private readonly linePositionMapperFactory: ILineMapperFactory; + private readonly linePositionMapperFactory: ILineBreaksComputerFactory; private hiddenAreasIds!: string[]; - constructor(model: ITextModel, linePositionMapperFactory: ILineMapperFactory, tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent) { + constructor(model: ITextModel, linePositionMapperFactory: ILineBreaksComputerFactory, tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent) { this.model = model; this._validModelVersionId = -1; this.tabSize = tabSize; @@ -170,7 +290,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { this.wrappingIndent = wrappingIndent; this.linePositionMapperFactory = linePositionMapperFactory; - this._constructLines(true); + this._constructLines(/*resetHiddenAreas*/true, null); } public dispose(): void { @@ -181,19 +301,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return new CoordinatesConverter(this); } - private _ensureValidState(): void { - let modelVersion = this.model.getVersionId(); - if (modelVersion !== this._validModelVersionId) { - // This is pretty bad, it means we lost track of the model... - throw new Error(`ViewModel is out of sync with Model!`); - } - if (this.lines.length !== this.model.getLineCount()) { - // This is pretty bad, it means we lost track of the model... - this._constructLines(false); - } - } - - private _constructLines(resetHiddenAreas: boolean): void { + private _constructLines(resetHiddenAreas: boolean, previousLineBreaks: ((LineBreakData | null)[]) | null): void { this.lines = []; if (resetHiddenAreas) { @@ -201,8 +309,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } let linesContent = this.model.getLinesContent(); - let lineCount = linesContent.length; - let values = new Uint32Array(lineCount); + const lineCount = linesContent.length; + const lineBreaksComputer = this.linePositionMapperFactory.createLineBreaksComputer(this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent); + for (let i = 0; i < lineCount; i++) { + lineBreaksComputer.addRequest(linesContent[i], previousLineBreaks ? previousLineBreaks[i] : null); + } + const linesBreaks = lineBreaksComputer.finalize(); + + let values: number[] = []; let hiddenAreas = this.hiddenAreasIds.map((areaId) => this.model.getDecorationRange(areaId)!).sort(Range.compareRangesUsingStarts); let hiddenAreaStart = 1, hiddenAreaEnd = 0; @@ -220,14 +334,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } let isInHiddenArea = (lineNumber >= hiddenAreaStart && lineNumber <= hiddenAreaEnd); - let line = createSplitLine(this.linePositionMapperFactory, linesContent[i], this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent, !isInHiddenArea); + let line = createSplitLine(linesBreaks[i], !isInHiddenArea); values[i] = line.getViewLineCount(); this.lines[i] = line; } this._validModelVersionId = this.model.getVersionId(); - this.prefixSumComputer = new PrefixSumComputerWithCache(values); + this.prefixSumComputer = new LineNumberMapper(values); } public getHiddenAreas(): Range[] { @@ -351,7 +465,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } this.tabSize = newTabSize; - this._constructLines(false); + this._constructLines(/*resetHiddenAreas*/false, null); return true; } @@ -361,17 +475,31 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return false; } + const onlyWrappingColumnChanged = (this.wrappingIndent === wrappingIndent && this.wrappingColumn !== wrappingColumn && this.columnsForFullWidthChar === columnsForFullWidthChar); + this.wrappingIndent = wrappingIndent; this.wrappingColumn = wrappingColumn; this.columnsForFullWidthChar = columnsForFullWidthChar; - this._constructLines(false); + let previousLineBreaks: ((LineBreakData | null)[]) | null = null; + if (onlyWrappingColumnChanged) { + previousLineBreaks = []; + for (let i = 0, len = this.lines.length; i < len; i++) { + previousLineBreaks[i] = this.lines[i].getLineBreakData(); + } + } + + this._constructLines(/*resetHiddenAreas*/false, previousLineBreaks); return true; } + public createLineBreaksComputer(): ILineBreaksComputer { + return this.linePositionMapperFactory.createLineBreaksComputer(this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent); + } + public onModelFlushed(): void { - this._constructLines(true); + this._constructLines(/*resetHiddenAreas*/true, null); } public onModelLinesDeleted(versionId: number, fromLineNumber: number, toLineNumber: number): viewEvents.ViewLinesDeletedEvent | null { @@ -390,7 +518,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return new viewEvents.ViewLinesDeletedEvent(outputFromLineNumber, outputToLineNumber); } - public onModelLinesInserted(versionId: number, fromLineNumber: number, _toLineNumber: number, text: string[]): viewEvents.ViewLinesInsertedEvent | null { + public onModelLinesInserted(versionId: number, fromLineNumber: number, _toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null { if (versionId <= this._validModelVersionId) { // Here we check for versionId in case the lines were reconstructed in the meantime. // We don't want to apply stale change events on top of a newer read model state. @@ -411,10 +539,10 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let totalOutputLineCount = 0; let insertLines: ISplitLine[] = []; - let insertPrefixSumValues = new Uint32Array(text.length); + let insertPrefixSumValues: number[] = []; - for (let i = 0, len = text.length; i < len; i++) { - let line = createSplitLine(this.linePositionMapperFactory, text[i], this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent, !isInHiddenArea); + for (let i = 0, len = lineBreaks.length; i < len; i++) { + let line = createSplitLine(lineBreaks[i], !isInHiddenArea); insertLines.push(line); let outputLineCount = line.getViewLineCount(); @@ -430,7 +558,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return new viewEvents.ViewLinesInsertedEvent(outputFromLineNumber, outputFromLineNumber + totalOutputLineCount - 1); } - public onModelLineChanged(versionId: number, lineNumber: number, newText: string): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] { + public onModelLineChanged(versionId: number, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] { if (versionId <= this._validModelVersionId) { // Here we check for versionId in case the lines were reconstructed in the meantime. // We don't want to apply stale change events on top of a newer read model state. @@ -441,7 +569,7 @@ export class SplitLinesCollection implements IViewModelLinesCollection { let oldOutputLineCount = this.lines[lineIndex].getViewLineCount(); let isVisible = this.lines[lineIndex].isVisible(); - let line = createSplitLine(this.linePositionMapperFactory, newText, this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent, isVisible); + let line = createSplitLine(lineBreakData, isVisible); this.lines[lineIndex] = line; let newOutputLineCount = this.lines[lineIndex].getViewLineCount(); @@ -488,7 +616,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineCount(): number { - this._ensureValidState(); return this.prefixSumComputer.getTotalValue(); } @@ -496,22 +623,14 @@ export class SplitLinesCollection implements IViewModelLinesCollection { if (viewLineNumber < 1) { return 1; } - let viewLineCount = this.getViewLineCount(); + const viewLineCount = this.getViewLineCount(); if (viewLineNumber > viewLineCount) { return viewLineCount; } - return viewLineNumber; - } - - /** - * Gives a hint that a lot of requests are about to come in for these line numbers. - */ - public warmUpLookupCache(viewStartLineNumber: number, viewEndLineNumber: number): void { - this.prefixSumComputer.warmUpCache(viewStartLineNumber - 1, viewEndLineNumber - 1); + return viewLineNumber | 0; } public getActiveIndentGuide(viewLineNumber: number, minLineNumber: number, maxLineNumber: number): IActiveIndentGuideInfo { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); minLineNumber = this._toValidViewLineNumber(minLineNumber); maxLineNumber = this._toValidViewLineNumber(maxLineNumber); @@ -531,7 +650,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLinesIndentGuides(viewStartLineNumber: number, viewEndLineNumber: number): number[] { - this._ensureValidState(); viewStartLineNumber = this._toValidViewLineNumber(viewStartLineNumber); viewEndLineNumber = this._toValidViewLineNumber(viewEndLineNumber); @@ -602,7 +720,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineContent(viewLineNumber: number): string { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); let lineIndex = r.index; @@ -612,7 +729,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineLength(viewLineNumber: number): number { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); let lineIndex = r.index; @@ -622,7 +738,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineMinColumn(viewLineNumber: number): number { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); let lineIndex = r.index; @@ -632,7 +747,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineMaxColumn(viewLineNumber: number): number { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); let lineIndex = r.index; @@ -642,7 +756,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLineData(viewLineNumber: number): ViewLineData { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); let lineIndex = r.index; @@ -652,7 +765,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public getViewLinesData(viewStartLineNumber: number, viewEndLineNumber: number, needed: boolean[]): ViewLineData[] { - this._ensureValidState(); viewStartLineNumber = this._toValidViewLineNumber(viewStartLineNumber); viewEndLineNumber = this._toValidViewLineNumber(viewEndLineNumber); @@ -691,7 +803,6 @@ export class SplitLinesCollection implements IViewModelLinesCollection { } public validateViewPosition(viewLineNumber: number, viewColumn: number, expectedModelPosition: Position): Position { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); @@ -719,8 +830,13 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return this.convertModelPositionToViewPosition(expectedModelPosition.lineNumber, expectedModelPosition.column); } + public validateViewRange(viewRange: Range, expectedModelRange: Range): Range { + const validViewStart = this.validateViewPosition(viewRange.startLineNumber, viewRange.startColumn, expectedModelRange.getStartPosition()); + const validViewEnd = this.validateViewPosition(viewRange.endLineNumber, viewRange.endColumn, expectedModelRange.getEndPosition()); + return new Range(validViewStart.lineNumber, validViewStart.column, validViewEnd.lineNumber, validViewEnd.column); + } + public convertViewPositionToModelPosition(viewLineNumber: number, viewColumn: number): Position { - this._ensureValidState(); viewLineNumber = this._toValidViewLineNumber(viewLineNumber); let r = this.prefixSumComputer.getIndexOf(viewLineNumber - 1); @@ -732,8 +848,13 @@ export class SplitLinesCollection implements IViewModelLinesCollection { return this.model.validatePosition(new Position(lineIndex + 1, inputColumn)); } + public convertViewRangeToModelRange(viewRange: Range): Range { + const start = this.convertViewPositionToModelPosition(viewRange.startLineNumber, viewRange.startColumn); + const end = this.convertViewPositionToModelPosition(viewRange.endLineNumber, viewRange.endColumn); + return new Range(start.lineNumber, start.column, end.lineNumber, end.column); + } + public convertModelPositionToViewPosition(_modelLineNumber: number, _modelColumn: number): Position { - this._ensureValidState(); const validPosition = this.model.validatePosition(new Position(_modelLineNumber, _modelColumn)); const inputLineNumber = validPosition.lineNumber; @@ -898,6 +1019,10 @@ class VisibleIdentitySplitLine implements ISplitLine { return InvisibleIdentitySplitLine.INSTANCE; } + public getLineBreakData(): LineBreakData | null { + return null; + } + public getViewLineCount(): number { return 1; } @@ -926,6 +1051,7 @@ class VisibleIdentitySplitLine implements ISplitLine { false, 1, lineContent.length + 1, + 0, lineTokens.inflate() ); } @@ -968,6 +1094,10 @@ class InvisibleIdentitySplitLine implements ISplitLine { return VisibleIdentitySplitLine.INSTANCE; } + public getLineBreakData(): LineBreakData | null { + return null; + } + public getViewLineCount(): number { return 0; } @@ -1011,18 +1141,11 @@ class InvisibleIdentitySplitLine implements ISplitLine { export class SplitLine implements ISplitLine { - private readonly positionMapper: ILineMapping; - private readonly outputLineCount: number; - - private readonly wrappedIndent: string; - private readonly wrappedIndentLength: number; + private readonly _lineBreakData: LineBreakData; private _isVisible: boolean; - constructor(positionMapper: ILineMapping, isVisible: boolean) { - this.positionMapper = positionMapper; - this.wrappedIndent = this.positionMapper.getWrappedLinesIndent(); - this.wrappedIndentLength = this.wrappedIndent.length; - this.outputLineCount = this.positionMapper.getOutputLineCount(); + constructor(lineBreakData: LineBreakData, isVisible: boolean) { + this._lineBreakData = lineBreakData; this._isVisible = isVisible; } @@ -1035,22 +1158,26 @@ export class SplitLine implements ISplitLine { return this; } + public getLineBreakData(): LineBreakData | null { + return this._lineBreakData; + } + public getViewLineCount(): number { if (!this._isVisible) { return 0; } - return this.outputLineCount; + return this._lineBreakData.breakOffsets.length; } private getInputStartOffsetOfOutputLineIndex(outputLineIndex: number): number { - return this.positionMapper.getInputOffsetOfOutputPosition(outputLineIndex, 0); + return LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets, outputLineIndex, 0); } private getInputEndOffsetOfOutputLineIndex(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): number { - if (outputLineIndex + 1 === this.outputLineCount) { + if (outputLineIndex + 1 === this._lineBreakData.breakOffsets.length) { return model.getLineMaxColumn(modelLineNumber) - 1; } - return this.positionMapper.getInputOffsetOfOutputPosition(outputLineIndex + 1, 0); + return LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets, outputLineIndex + 1, 0); } public getViewLineContent(model: ISimpleModel, modelLineNumber: number, outputLineIndex: number): string { @@ -1067,7 +1194,7 @@ export class SplitLine implements ISplitLine { }); if (outputLineIndex > 0) { - r = this.wrappedIndent + r; + r = spaces(this._lineBreakData.wrappedTextIndentLength) + r; } return r; @@ -1082,7 +1209,7 @@ export class SplitLine implements ISplitLine { let r = endOffset - startOffset; if (outputLineIndex > 0) { - r = this.wrappedIndent.length + r; + r = this._lineBreakData.wrappedTextIndentLength + r; } return r; @@ -1093,7 +1220,7 @@ export class SplitLine implements ISplitLine { throw new Error('Not supported'); } if (outputLineIndex > 0) { - return this.wrappedIndentLength + 1; + return this._lineBreakData.wrappedTextIndentLength + 1; } return 1; } @@ -1121,25 +1248,28 @@ export class SplitLine implements ISplitLine { }); if (outputLineIndex > 0) { - lineContent = this.wrappedIndent + lineContent; + lineContent = spaces(this._lineBreakData.wrappedTextIndentLength) + lineContent; } - let minColumn = (outputLineIndex > 0 ? this.wrappedIndentLength + 1 : 1); + let minColumn = (outputLineIndex > 0 ? this._lineBreakData.wrappedTextIndentLength + 1 : 1); let maxColumn = lineContent.length + 1; let continuesWithWrappedLine = (outputLineIndex + 1 < this.getViewLineCount()); let deltaStartIndex = 0; if (outputLineIndex > 0) { - deltaStartIndex = this.wrappedIndentLength; + deltaStartIndex = this._lineBreakData.wrappedTextIndentLength; } let lineTokens = model.getLineTokens(modelLineNumber); + const startVisibleColumn = (outputLineIndex === 0 ? 0 : this._lineBreakData.breakOffsetsVisibleColumn[outputLineIndex - 1]); + return new ViewLineData( lineContent, continuesWithWrappedLine, minColumn, maxColumn, + startVisibleColumn, lineTokens.sliceAndInflate(startOffset, endOffset, deltaStartIndex) ); } @@ -1165,25 +1295,25 @@ export class SplitLine implements ISplitLine { } let adjustedColumn = outputColumn - 1; if (outputLineIndex > 0) { - if (adjustedColumn < this.wrappedIndentLength) { + if (adjustedColumn < this._lineBreakData.wrappedTextIndentLength) { adjustedColumn = 0; } else { - adjustedColumn -= this.wrappedIndentLength; + adjustedColumn -= this._lineBreakData.wrappedTextIndentLength; } } - return this.positionMapper.getInputOffsetOfOutputPosition(outputLineIndex, adjustedColumn) + 1; + return LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets, outputLineIndex, adjustedColumn) + 1; } public getViewPositionOfModelPosition(deltaLineNumber: number, inputColumn: number): Position { if (!this._isVisible) { throw new Error('Not supported'); } - let r = this.positionMapper.getOutputPositionOfInputOffset(inputColumn - 1); + let r = LineBreakData.getOutputPositionOfInputOffset(this._lineBreakData.breakOffsets, inputColumn - 1); let outputLineIndex = r.outputLineIndex; let outputColumn = r.outputOffset + 1; if (outputLineIndex > 0) { - outputColumn += this.wrappedIndentLength; + outputColumn += this._lineBreakData.wrappedTextIndentLength; } // console.log('in -> out ' + deltaLineNumber + ',' + inputColumn + ' ===> ' + (deltaLineNumber+outputLineIndex) + ',' + outputColumn); @@ -1194,21 +1324,33 @@ export class SplitLine implements ISplitLine { if (!this._isVisible) { throw new Error('Not supported'); } - const r = this.positionMapper.getOutputPositionOfInputOffset(inputColumn - 1); + const r = LineBreakData.getOutputPositionOfInputOffset(this._lineBreakData.breakOffsets, inputColumn - 1); return (deltaLineNumber + r.outputLineIndex); } } -function createSplitLine(linePositionMapperFactory: ILineMapperFactory, text: string, tabSize: number, wrappingColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, isVisible: boolean): ISplitLine { - let positionMapper = linePositionMapperFactory.createLineMapping(text, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent); - if (positionMapper === null) { +let _spaces: string[] = ['']; +function spaces(count: number): string { + if (count >= _spaces.length) { + for (let i = 1; i <= count; i++) { + _spaces[i] = _makeSpaces(i); + } + } + return _spaces[count]; +} +function _makeSpaces(count: number): string { + return new Array(count + 1).join(' '); +} + +function createSplitLine(lineBreakData: LineBreakData | null, isVisible: boolean): ISplitLine { + if (lineBreakData === null) { // No mapping needed if (isVisible) { return VisibleIdentitySplitLine.INSTANCE; } return InvisibleIdentitySplitLine.INSTANCE; } else { - return new SplitLine(positionMapper, isVisible); + return new SplitLine(lineBreakData, isVisible); } } @@ -1298,6 +1440,18 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return false; } + public createLineBreaksComputer(): ILineBreaksComputer { + let result: null[] = []; + return { + addRequest: (lineText: string, previousLineBreakData: LineBreakData | null) => { + result.push(null); + }, + finalize: () => { + return result; + } + }; + } + public onModelFlushed(): void { } @@ -1305,11 +1459,11 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return new viewEvents.ViewLinesDeletedEvent(fromLineNumber, toLineNumber); } - public onModelLinesInserted(_versionId: number, fromLineNumber: number, toLineNumber: number, _text: string[]): viewEvents.ViewLinesInsertedEvent | null { + public onModelLinesInserted(_versionId: number, fromLineNumber: number, toLineNumber: number, lineBreaks: (LineBreakData | null)[]): viewEvents.ViewLinesInsertedEvent | null { return new viewEvents.ViewLinesInsertedEvent(fromLineNumber, toLineNumber); } - public onModelLineChanged(_versionId: number, lineNumber: number, _newText: string): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] { + public onModelLineChanged(_versionId: number, lineNumber: number, lineBreakData: LineBreakData | null): [boolean, viewEvents.ViewLinesChangedEvent | null, viewEvents.ViewLinesInsertedEvent | null, viewEvents.ViewLinesDeletedEvent | null] { return [false, new viewEvents.ViewLinesChangedEvent(lineNumber, lineNumber), null, null]; } @@ -1320,9 +1474,6 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { return this.model.getLineCount(); } - public warmUpLookupCache(_viewStartLineNumber: number, _viewEndLineNumber: number): void { - } - public getActiveIndentGuide(viewLineNumber: number, _minLineNumber: number, _maxLineNumber: number): IActiveIndentGuideInfo { return { startLineNumber: viewLineNumber, @@ -1364,6 +1515,7 @@ export class IdentityLinesCollection implements IViewModelLinesCollection { false, 1, lineContent.length + 1, + 0, lineTokens.inflate() ); } diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index a3abd74ee4ebced14c2e73d930602c7e088200df..a79c030bf966658283ef50f43d91b2b26d3df7c7 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -174,6 +174,10 @@ export class ViewLineData { * The maximum allowed column at this view line. */ public readonly maxColumn: number; + /** + * The visible column at the start of the line (after the fauxIndent). + */ + public readonly startVisibleColumn: number; /** * The tokens at this view line. */ @@ -184,12 +188,14 @@ export class ViewLineData { continuesWithWrappedLine: boolean, minColumn: number, maxColumn: number, + startVisibleColumn: number, tokens: IViewLineTokens ) { this.content = content; this.continuesWithWrappedLine = continuesWithWrappedLine; this.minColumn = minColumn; this.maxColumn = maxColumn; + this.startVisibleColumn = startVisibleColumn; this.tokens = tokens; } } @@ -231,6 +237,10 @@ export class ViewLineRenderingData { * The tab size for this view model. */ public readonly tabSize: number; + /** + * The visible column at the start of the line (after the fauxIndent) + */ + public readonly startVisibleColumn: number; constructor( minColumn: number, @@ -241,7 +251,8 @@ export class ViewLineRenderingData { mightContainNonBasicASCII: boolean, tokens: IViewLineTokens, inlineDecorations: InlineDecoration[], - tabSize: number + tabSize: number, + startVisibleColumn: number ) { this.minColumn = minColumn; this.maxColumn = maxColumn; @@ -254,6 +265,7 @@ export class ViewLineRenderingData { this.tokens = tokens; this.inlineDecorations = inlineDecorations; this.tabSize = tabSize; + this.startVisibleColumn = startVisibleColumn; } public static isBasicASCII(lineContent: string, mightContainNonBasicASCII: boolean): boolean { diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index e08af64ebf9aa734a5d892f7594da61e144aa507..718c9fc090e18fe5d893649617c37c2e900c68ea 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -18,8 +18,7 @@ import { tokenizeLineToHTML } from 'vs/editor/common/modes/textToHtmlTokenizer'; import { MinimapTokensColorTracker } from 'vs/editor/common/viewModel/minimapTokensColorTracker'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { ViewLayout } from 'vs/editor/common/viewLayout/viewLayout'; -import { CharacterHardWrappingLineMapperFactory } from 'vs/editor/common/viewModel/characterHardWrappingLineMapper'; -import { IViewModelLinesCollection, IdentityLinesCollection, SplitLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; +import { IViewModelLinesCollection, IdentityLinesCollection, SplitLinesCollection, ILineBreaksComputerFactory } from 'vs/editor/common/viewModel/splitLinesCollection'; import { ICoordinatesConverter, IOverviewRulerDecorations, IViewModel, MinimapLinesRenderingData, ViewLineData, ViewLineRenderingData, ViewModelDecoration } from 'vs/editor/common/viewModel/viewModel'; import { ViewModelDecorations } from 'vs/editor/common/viewModel/viewModelDecorations'; import { ITheme } from 'vs/platform/theme/common/themeService'; @@ -43,7 +42,13 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel public readonly viewLayout: ViewLayout; private readonly decorations: ViewModelDecorations; - constructor(editorId: number, configuration: editorCommon.IConfiguration, model: ITextModel, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { + constructor( + editorId: number, + configuration: editorCommon.IConfiguration, + model: ITextModel, + lineMapperFactory: ILineBreaksComputerFactory, + scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable + ) { super(); this.editorId = editorId; @@ -63,20 +68,11 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel const options = this.configuration.options; const wrappingInfo = options.get(EditorOption.wrappingInfo); const fontInfo = options.get(EditorOption.fontInfo); - const wordWrapBreakAfterCharacters = options.get(EditorOption.wordWrapBreakAfterCharacters); - const wordWrapBreakBeforeCharacters = options.get(EditorOption.wordWrapBreakBeforeCharacters); - const wordWrapBreakObtrusiveCharacters = options.get(EditorOption.wordWrapBreakObtrusiveCharacters); const wrappingIndent = options.get(EditorOption.wrappingIndent); - let hardWrappingLineMapperFactory = new CharacterHardWrappingLineMapperFactory( - wordWrapBreakBeforeCharacters, - wordWrapBreakAfterCharacters, - wordWrapBreakObtrusiveCharacters - ); - this.lines = new SplitLinesCollection( this.model, - hardWrappingLineMapperFactory, + lineMapperFactory, this.model.getOptions().tabSize, wrappingInfo.wrappingColumn, fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth, @@ -200,8 +196,26 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel const changes = e.changes; const versionId = e.versionId; - for (let j = 0, lenJ = changes.length; j < lenJ; j++) { - const change = changes[j]; + // Do a first pass to compute line mappings, and a second pass to actually interpret them + const lineBreaksComputer = this.lines.createLineBreaksComputer(); + for (const change of changes) { + switch (change.changeType) { + case textModelEvents.RawContentChangedType.LinesInserted: { + for (const line of change.detail) { + lineBreaksComputer.addRequest(line, null); + } + break; + } + case textModelEvents.RawContentChangedType.LineChanged: { + lineBreaksComputer.addRequest(change.detail, null); + break; + } + } + } + const lineBreaks = lineBreaksComputer.finalize(); + let lineBreaksOffset = 0; + + for (const change of changes) { switch (change.changeType) { case textModelEvents.RawContentChangedType.Flush: { @@ -222,7 +236,10 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel break; } case textModelEvents.RawContentChangedType.LinesInserted: { - const linesInsertedEvent = this.lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, change.detail); + const insertedLineBreaks = lineBreaks.slice(lineBreaksOffset, lineBreaksOffset + change.detail.length); + lineBreaksOffset += change.detail.length; + + const linesInsertedEvent = this.lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks); if (linesInsertedEvent !== null) { eventsCollector.emit(linesInsertedEvent); this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); @@ -231,7 +248,10 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel break; } case textModelEvents.RawContentChangedType.LineChanged: { - const [lineMappingChanged, linesChangedEvent, linesInsertedEvent, linesDeletedEvent] = this.lines.onModelLineChanged(versionId, change.lineNumber, change.detail); + const changedLineBreakData = lineBreaks[lineBreaksOffset]; + lineBreaksOffset++; + + const [lineMappingChanged, linesChangedEvent, linesInsertedEvent, linesDeletedEvent] = this.lines.onModelLineChanged(versionId, change.lineNumber, changedLineBreakData); hadModelLineChangeThatChangedLineMapping = lineMappingChanged; if (linesChangedEvent) { eventsCollector.emit(linesChangedEvent); @@ -475,8 +495,6 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel * Gives a hint that a lot of requests are about to come in for these line numbers. */ public setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void { - this.lines.warmUpLookupCache(startLineNumber, endLineNumber); - this.viewportStartLine = startLineNumber; let position = this.coordinatesConverter.convertViewPositionToModelPosition(new Position(startLineNumber, this.getLineMinColumn(startLineNumber))); this.viewportStartLineTrackedRange = this.model._setTrackedRange(this.viewportStartLineTrackedRange, new Range(position.lineNumber, position.column, position.lineNumber, position.column), TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); @@ -546,7 +564,8 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel mightContainNonBasicASCII, lineData.tokens, inlineDecorations, - tabSize + tabSize, + lineData.startVisibleColumn ); } diff --git a/src/vs/editor/contrib/comment/lineCommentCommand.ts b/src/vs/editor/contrib/comment/lineCommentCommand.ts index 5c3716ed79cdbd1b4ffd32701f478d455a4b5510..2eecd5ae2c51e6b6ce33a7fb5280c53cd3df525d 100644 --- a/src/vs/editor/contrib/comment/lineCommentCommand.ts +++ b/src/vs/editor/contrib/comment/lineCommentCommand.ts @@ -381,7 +381,6 @@ export class LineCommentCommand implements editorCommon.ICommand { return res; } - // TODO@Alex -> duplicated in characterHardWrappingLineMapper private static nextVisibleColumn(currentVisibleColumn: number, tabSize: number, isTab: boolean, columnSize: number): number { if (isTab) { return currentVisibleColumn + (tabSize - (currentVisibleColumn % tabSize)); diff --git a/src/vs/editor/standalone/browser/colorizer.ts b/src/vs/editor/standalone/browser/colorizer.ts index d72d141046bcfb400283214105c8e988ba8f44ed..679734f865259a87626260df9cd59c68ecb7f711 100644 --- a/src/vs/editor/standalone/browser/colorizer.ts +++ b/src/vs/editor/standalone/browser/colorizer.ts @@ -125,6 +125,7 @@ export class Colorizer { [], tabSize, 0, + 0, -1, 'none', false, @@ -193,6 +194,7 @@ function _fakeColorize(lines: string[], tabSize: number): string { [], tabSize, 0, + 0, -1, 'none', false, @@ -230,6 +232,7 @@ function _actualColorize(lines: string[], tabSize: number, tokenizationSupport: [], tabSize, 0, + 0, -1, 'none', false, diff --git a/src/vs/editor/test/browser/commands/sideEditing.test.ts b/src/vs/editor/test/browser/commands/sideEditing.test.ts index 250f2ff7960926d1ecdb3c84869a40502e59a495..67ae4e4d1749b681ee97f737c4ba3f2b1494aeb7 100644 --- a/src/vs/editor/test/browser/commands/sideEditing.test.ts +++ b/src/vs/editor/test/browser/commands/sideEditing.test.ts @@ -14,6 +14,7 @@ import { TextModel } from 'vs/editor/common/model/textModel'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; +import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; function testCommand(lines: string[], selections: Selection[], edits: IIdentifiedSingleEditOperation[], expectedLines: string[], expectedSelections: Selection[]): void { withTestCodeEditor(lines, {}, (editor, cursor) => { @@ -200,7 +201,7 @@ suite('SideEditing', () => { function _runTest(selection: Selection, editRange: Range, editText: string, editForceMoveMarkers: boolean, expected: Selection, msg: string): void { const model = TextModel.createFromString(LINES.join('\n')); const config = new TestConfiguration({}); - const viewModel = new ViewModel(0, config, model, null!); + const viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!); const cursor = new Cursor(config, model, viewModel); cursor.setSelections('tests', [selection]); diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index d305b2e5ed8c7520971550e8dc8bd70d7518dc2c..7d82c86f55e5fdd6bae0d8682ffdc16f721fa4c4 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -25,6 +25,7 @@ import { IRelaxedTextModelCreationOptions, createTextModel } from 'vs/editor/tes import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; +import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; const H = Handler; @@ -152,7 +153,7 @@ suite('Editor Controller - Cursor', () => { thisModel = createTextModel(text); thisConfiguration = new TestConfiguration({}); - thisViewModel = new ViewModel(0, thisConfiguration, thisModel, null!); + thisViewModel = new ViewModel(0, thisConfiguration, thisModel, MonospaceLineBreaksComputerFactory.create(thisConfiguration.options), null!); thisCursor = new Cursor(thisConfiguration, thisModel, thisViewModel); }); @@ -776,7 +777,7 @@ suite('Editor Controller - Cursor', () => { 'var newer = require("gulp-newer");', ].join('\n')); const config = new TestConfiguration({}); - const viewModel = new ViewModel(0, config, model, null!); + const viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!); const cursor = new Cursor(config, model, viewModel); moveTo(cursor, 1, 4, false); @@ -816,7 +817,7 @@ suite('Editor Controller - Cursor', () => { '', ].join('\n')); const config = new TestConfiguration({}); - const viewModel = new ViewModel(0, config, model, null!); + const viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!); const cursor = new Cursor(config, model, viewModel); moveTo(cursor, 10, 10, false); @@ -880,7 +881,7 @@ suite('Editor Controller - Cursor', () => { '', ].join('\n')); const config = new TestConfiguration({}); - const viewModel = new ViewModel(0, config, model, null!); + const viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!); const cursor = new Cursor(config, model, viewModel); moveTo(cursor, 10, 10, false); @@ -929,7 +930,7 @@ suite('Editor Controller - Cursor', () => { 'var newer = require("gulp-newer");', ].join('\n')); const config = new TestConfiguration({}); - const viewModel = new ViewModel(0, config, model, null!); + const viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!); const cursor = new Cursor(config, model, viewModel); moveTo(cursor, 1, 4, false); @@ -2074,7 +2075,7 @@ suite('Editor Controller - Regression tests', () => { wordWrap: 'wordWrapColumn', wordWrapColumn: 100 }); - const viewModel = new ViewModel(0, config, model, null!); + const viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!); const cursor = new Cursor(config, model, viewModel); moveTo(cursor, 1, 43, false); @@ -3834,7 +3835,7 @@ function usingCursor(opts: ICursorOpts, callback: (model: TextModel, cursor: Cur let model = createTextModel(opts.text.join('\n'), opts.modelOpts, opts.languageIdentifier); model.forceTokenization(model.getLineCount()); let config = new TestConfiguration(opts.editorOpts || {}); - let viewModel = new ViewModel(0, config, model, null!); + let viewModel = new ViewModel(0, config, model, MonospaceLineBreaksComputerFactory.create(config.options), null!); let cursor = new Cursor(config, model, viewModel); callback(model, cursor); diff --git a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts index a58db2d6c3fff24bec485043692c55f923563d29..117cf54372d521a4993da02fed195df949960370 100644 --- a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts +++ b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts @@ -13,6 +13,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { TextModel } from 'vs/editor/common/model/textModel'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; +import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; suite('Cursor move command test', () => { @@ -32,7 +33,7 @@ suite('Cursor move command test', () => { thisModel = TextModel.createFromString(text); thisConfiguration = new TestConfiguration({}); - thisViewModel = new ViewModel(0, thisConfiguration, thisModel, null!); + thisViewModel = new ViewModel(0, thisConfiguration, thisModel, MonospaceLineBreaksComputerFactory.create(thisConfiguration.options), null!); thisCursor = new Cursor(thisConfiguration, thisModel, thisViewModel); }); diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 040629e325b6b1d79b71f9302f9882cc4de71e3f..ccb50958ccdf208c2291fea50f93536df9eb8405 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -38,6 +38,7 @@ suite('viewLineRenderer.renderLine', () => { [], tabSize, 0, + 0, -1, 'none', false, @@ -88,6 +89,7 @@ suite('viewLineRenderer.renderLine', () => { [], tabSize, 0, + 0, -1, 'none', false, @@ -140,6 +142,7 @@ suite('viewLineRenderer.renderLine', () => { ]), [], 4, + 0, 10, 6, 'boundary', @@ -232,6 +235,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'boundary', @@ -295,6 +299,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -358,6 +363,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -398,6 +404,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -429,6 +436,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -530,6 +538,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -569,6 +578,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -599,6 +609,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -646,6 +657,7 @@ suite('viewLineRenderer.renderLine', () => { lineParts, [], 4, + 0, 10, -1, 'none', @@ -728,6 +740,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens(tokens), [], 4, + 0, 10, -1, renderWhitespace, @@ -754,6 +767,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(21, 3)]), [new LineDecoration(1, 22, 'link', InlineDecorationType.Regular)], 4, + 0, 10, -1, 'none', @@ -794,6 +808,7 @@ suite('viewLineRenderer.renderLine 2', () => { new LineDecoration(13, 51, 'detected-link', InlineDecorationType.Regular) ], 4, + 0, 10, -1, 'none', @@ -1209,6 +1224,7 @@ suite('viewLineRenderer.renderLine 2', () => { new LineDecoration(2, 8, 'c', InlineDecorationType.Regular), ], 4, + 0, 10, -1, 'none', @@ -1250,6 +1266,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(4, 3)]), [new LineDecoration(1, 2, 'before', InlineDecorationType.Before)], 4, + 0, 10, -1, 'all', @@ -1283,6 +1300,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(4, 3)]), [new LineDecoration(2, 3, 'before', InlineDecorationType.Before)], 4, + 0, 10, -1, 'all', @@ -1317,6 +1335,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(0, 3)]), [new LineDecoration(1, 2, 'before', InlineDecorationType.Before)], 4, + 0, 10, -1, 'all', @@ -1347,6 +1366,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(7, 3)]), [new LineDecoration(7, 8, 'inline-folded', InlineDecorationType.After)], 2, + 0, 10, 10000, 'none', @@ -1381,6 +1401,7 @@ suite('viewLineRenderer.renderLine 2', () => { new LineDecoration(0, 1, 'after', InlineDecorationType.After), ], 2, + 0, 10, 10000, 'none', @@ -1414,6 +1435,7 @@ suite('viewLineRenderer.renderLine 2', () => { new LineDecoration(3, 3, 'ced-TextEditorDecorationType2-5e9b9b3f-4 ced-TextEditorDecorationType2-4', InlineDecorationType.After), ], 4, + 0, 10, 10000, 'none', @@ -1445,6 +1467,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(15, 3)]), [], 4, + 0, 10, 10000, 'none', @@ -1475,6 +1498,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(15, 3)]), [], 4, + 0, 10, 10000, 'all', @@ -1511,6 +1535,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(53, 3)]), [], 4, + 0, 10, 10000, 'none', @@ -1541,6 +1566,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(100, 3)]), [], 4, + 0, 10, 10000, 'none', @@ -1573,6 +1599,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(105, 3)]), [], 4, + 0, 10, 10000, 'none', @@ -1604,6 +1631,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(59, 3)]), [], 4, + 0, 10, 10000, 'boundary', @@ -1633,6 +1661,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(194, 3)]), [], 4, + 0, 10, 10000, 'none', @@ -1666,6 +1695,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens([createPart(194, 3)]), [], 4, + 0, 10, 10000, 'none', @@ -1695,6 +1725,7 @@ suite('viewLineRenderer.renderLine 2', () => { createViewLineTokens(parts), [], tabSize, + 0, 10, -1, 'none', diff --git a/src/vs/editor/test/common/viewModel/characterHardWrappingLineMapper.test.ts b/src/vs/editor/test/common/viewModel/characterHardWrappingLineMapper.test.ts deleted file mode 100644 index aec12368fa1430be697aecb371685ecca8a38319..0000000000000000000000000000000000000000 --- a/src/vs/editor/test/common/viewModel/characterHardWrappingLineMapper.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import * as assert from 'assert'; -import { WrappingIndent } from 'vs/editor/common/config/editorOptions'; -import { CharacterHardWrappingLineMapperFactory } from 'vs/editor/common/viewModel/characterHardWrappingLineMapper'; -import { ILineMapperFactory, ILineMapping } from 'vs/editor/common/viewModel/splitLinesCollection'; - -function assertLineMapping(factory: ILineMapperFactory, tabSize: number, breakAfter: number, annotatedText: string, wrappingIndent = WrappingIndent.None): ILineMapping | null { - // Create version of `annotatedText` with line break markers removed - let rawText = ''; - let currentLineIndex = 0; - let lineIndices: number[] = []; - for (let i = 0, len = annotatedText.length; i < len; i++) { - if (annotatedText.charAt(i) === '|') { - currentLineIndex++; - } else { - rawText += annotatedText.charAt(i); - lineIndices[rawText.length - 1] = currentLineIndex; - } - } - - const mapper = factory.createLineMapping(rawText, tabSize, breakAfter, 2, wrappingIndent); - - // Insert line break markers again, according to algorithm - let actualAnnotatedText = ''; - if (mapper) { - let previousLineIndex = 0; - for (let i = 0, len = rawText.length; i < len; i++) { - let r = mapper.getOutputPositionOfInputOffset(i); - if (previousLineIndex !== r.outputLineIndex) { - previousLineIndex = r.outputLineIndex; - actualAnnotatedText += '|'; - } - actualAnnotatedText += rawText.charAt(i); - } - } else { - // No wrapping - actualAnnotatedText = rawText; - } - - assert.equal(actualAnnotatedText, annotatedText); - - return mapper; -} - -suite('Editor ViewModel - CharacterHardWrappingLineMapper', () => { - test('CharacterHardWrappingLineMapper', () => { - - let factory = new CharacterHardWrappingLineMapperFactory('(', ')', '.'); - - // Empty string - assertLineMapping(factory, 4, 5, ''); - - // No wrapping if not necessary - assertLineMapping(factory, 4, 5, 'aaa'); - assertLineMapping(factory, 4, 5, 'aaaaa'); - assertLineMapping(factory, 4, -1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); - - // Acts like hard wrapping if no char found - assertLineMapping(factory, 4, 5, 'aaaaa|a'); - - // Honors obtrusive wrapping character - assertLineMapping(factory, 4, 5, 'aaaaa|.'); - assertLineMapping(factory, 4, 5, 'aaaaa|a.|aaa.|aa'); - assertLineMapping(factory, 4, 5, 'aaaaa|a..|aaa.|aa'); - assertLineMapping(factory, 4, 5, 'aaaaa|a...|aaa.|aa'); - assertLineMapping(factory, 4, 5, 'aaaaa|a....|aaa.|aa'); - - // Honors tabs when computing wrapping position - assertLineMapping(factory, 4, 5, '\t'); - assertLineMapping(factory, 4, 5, '\ta|aa'); - assertLineMapping(factory, 4, 5, '\ta|\ta|a'); - assertLineMapping(factory, 4, 5, 'aa\ta'); - assertLineMapping(factory, 4, 5, 'aa\ta|a'); - - // Honors wrapping before characters (& gives it priority) - assertLineMapping(factory, 4, 5, 'aaa.|aa'); - assertLineMapping(factory, 4, 5, 'aaa|(.aa'); - - // Honors wrapping after characters (& gives it priority) - assertLineMapping(factory, 4, 5, 'aaa))|).aaa'); - assertLineMapping(factory, 4, 5, 'aaa))|)|.aaaa'); - assertLineMapping(factory, 4, 5, 'aaa)|()|.aaa'); - assertLineMapping(factory, 4, 5, 'aaa(|()|.aaa'); - assertLineMapping(factory, 4, 5, 'aa.(|()|.aaa'); - assertLineMapping(factory, 4, 5, 'aa.|(.)|.aaa'); - }); - - test('CharacterHardWrappingLineMapper - CJK and Kinsoku Shori', () => { - let factory = new CharacterHardWrappingLineMapperFactory('(', ')', '.'); - assertLineMapping(factory, 4, 5, 'aa \u5b89|\u5b89'); - assertLineMapping(factory, 4, 5, '\u3042 \u5b89|\u5b89'); - assertLineMapping(factory, 4, 5, '\u3042\u3042|\u5b89\u5b89'); - assertLineMapping(factory, 4, 5, 'aa |\u5b89)\u5b89|\u5b89'); - assertLineMapping(factory, 4, 5, 'aa \u3042|\u5b89\u3042)|\u5b89'); - assertLineMapping(factory, 4, 5, 'aa |(\u5b89aa|\u5b89'); - }); - - test('CharacterHardWrappingLineMapper - WrappingIndent.Same', () => { - let factory = new CharacterHardWrappingLineMapperFactory('', ' ', ''); - assertLineMapping(factory, 4, 38, ' *123456789012345678901234567890123456|7890', WrappingIndent.Same); - }); - - test('issue #16332: Scroll bar overlaying on top of text', () => { - let factory = new CharacterHardWrappingLineMapperFactory('', ' ', ''); - assertLineMapping(factory, 4, 24, 'a/ very/long/line/of/tex|t/that/expands/beyon|d/your/typical/line/|of/code/', WrappingIndent.Indent); - }); - - test('issue #35162: wrappingIndent not consistently working', () => { - let factory = new CharacterHardWrappingLineMapperFactory('', ' ', ''); - let mapper = assertLineMapping(factory, 4, 24, ' t h i s |i s |a l |o n |g l |i n |e', WrappingIndent.Indent); - assert.equal(mapper!.getWrappedLinesIndent(), ' \t'); - }); - - test('issue #75494: surrogate pairs', () => { - let factory = new CharacterHardWrappingLineMapperFactory('', ' ', ''); - assertLineMapping(factory, 4, 49, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇|👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬', WrappingIndent.Same); - }); - - test('CharacterHardWrappingLineMapper - WrappingIndent.DeepIndent', () => { - let factory = new CharacterHardWrappingLineMapperFactory('', ' ', ''); - let mapper = assertLineMapping(factory, 4, 26, ' W e A r e T e s t |i n g D e |e p I n d |e n t a t |i o n', WrappingIndent.DeepIndent); - assert.equal(mapper!.getWrappedLinesIndent(), ' \t\t'); - }); -}); diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..625c678ff4767cd37e9f8f1d90aade820c12ca5e --- /dev/null +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -0,0 +1,235 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as assert from 'assert'; +import { WrappingIndent, EditorOptions } from 'vs/editor/common/config/editorOptions'; +import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; +import { ILineBreaksComputerFactory, LineBreakData } from 'vs/editor/common/viewModel/splitLinesCollection'; + +function parseAnnotatedText(annotatedText: string): { text: string; indices: number[]; } { + let text = ''; + let currentLineIndex = 0; + let indices: number[] = []; + for (let i = 0, len = annotatedText.length; i < len; i++) { + if (annotatedText.charAt(i) === '|') { + currentLineIndex++; + } else { + text += annotatedText.charAt(i); + indices[text.length - 1] = currentLineIndex; + } + } + return { text: text, indices: indices }; +} + +function toAnnotatedText(text: string, lineBreakData: LineBreakData | null): string { + // Insert line break markers again, according to algorithm + let actualAnnotatedText = ''; + if (lineBreakData) { + let previousLineIndex = 0; + for (let i = 0, len = text.length; i < len; i++) { + let r = LineBreakData.getOutputPositionOfInputOffset(lineBreakData.breakOffsets, i); + if (previousLineIndex !== r.outputLineIndex) { + previousLineIndex = r.outputLineIndex; + actualAnnotatedText += '|'; + } + actualAnnotatedText += text.charAt(i); + } + } else { + // No wrapping + actualAnnotatedText = text; + } + return actualAnnotatedText; +} + +function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, text: string, previousLineBreakData: LineBreakData | null): LineBreakData | null { + const lineBreaksComputer = factory.createLineBreaksComputer(tabSize, breakAfter, columnsForFullWidthChar, wrappingIndent); + const previousLineBreakDataClone = previousLineBreakData ? new LineBreakData(previousLineBreakData.breakOffsets.slice(0), previousLineBreakData.breakOffsetsVisibleColumn.slice(0), previousLineBreakData.wrappedTextIndentLength) : null; + lineBreaksComputer.addRequest(text, previousLineBreakDataClone); + return lineBreaksComputer.finalize()[0]; +} + +function assertLineBreaks(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, annotatedText: string, wrappingIndent = WrappingIndent.None): LineBreakData | null { + // Create version of `annotatedText` with line break markers removed + const text = parseAnnotatedText(annotatedText).text; + const lineBreakData = getLineBreakData(factory, tabSize, breakAfter, 2, wrappingIndent, text, null); + const actualAnnotatedText = toAnnotatedText(text, lineBreakData); + + assert.equal(actualAnnotatedText, annotatedText); + + return lineBreakData; +} + +suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { + test('MonospaceLineBreaksComputer', () => { + + let factory = new MonospaceLineBreaksComputerFactory('(', '\t).'); + + // Empty string + assertLineBreaks(factory, 4, 5, ''); + + // No wrapping if not necessary + assertLineBreaks(factory, 4, 5, 'aaa'); + assertLineBreaks(factory, 4, 5, 'aaaaa'); + assertLineBreaks(factory, 4, -1, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + + // Acts like hard wrapping if no char found + assertLineBreaks(factory, 4, 5, 'aaaaa|a'); + + // Honors wrapping character + assertLineBreaks(factory, 4, 5, 'aaaaa|.'); + assertLineBreaks(factory, 4, 5, 'aaaaa|a.|aaa.|aa'); + assertLineBreaks(factory, 4, 5, 'aaaaa|a..|aaa.|aa'); + assertLineBreaks(factory, 4, 5, 'aaaaa|a...|aaa.|aa'); + assertLineBreaks(factory, 4, 5, 'aaaaa|a....|aaa.|aa'); + + // Honors tabs when computing wrapping position + assertLineBreaks(factory, 4, 5, '\t'); + assertLineBreaks(factory, 4, 5, '\t|aaa'); + assertLineBreaks(factory, 4, 5, '\t|a\t|aa'); + assertLineBreaks(factory, 4, 5, 'aa\ta'); + assertLineBreaks(factory, 4, 5, 'aa\t|aa'); + + // Honors wrapping before characters (& gives it priority) + assertLineBreaks(factory, 4, 5, 'aaa.|aa'); + assertLineBreaks(factory, 4, 5, 'aaa(.|aa'); + + // Honors wrapping after characters (& gives it priority) + assertLineBreaks(factory, 4, 5, 'aaa))|).aaa'); + assertLineBreaks(factory, 4, 5, 'aaa))|).|aaaa'); + assertLineBreaks(factory, 4, 5, 'aaa)|().|aaa'); + assertLineBreaks(factory, 4, 5, 'aaa(|().|aaa'); + assertLineBreaks(factory, 4, 5, 'aa.(|().|aaa'); + assertLineBreaks(factory, 4, 5, 'aa.(.|).aaa'); + }); + + function assertIncrementalLineBreaks(factory: ILineBreaksComputerFactory, text: string, tabSize: number, breakAfter1: number, annotatedText1: string, breakAfter2: number, annotatedText2: string, wrappingIndent = WrappingIndent.None): void { + // sanity check the test + assert.equal(text, parseAnnotatedText(annotatedText1).text); + assert.equal(text, parseAnnotatedText(annotatedText2).text); + + // check that the direct mapping is ok for 1 + const directLineBreakData1 = getLineBreakData(factory, tabSize, breakAfter1, 2, wrappingIndent, text, null); + assert.equal(toAnnotatedText(text, directLineBreakData1), annotatedText1); + + // check that the direct mapping is ok for 2 + const directLineBreakData2 = getLineBreakData(factory, tabSize, breakAfter2, 2, wrappingIndent, text, null); + assert.equal(toAnnotatedText(text, directLineBreakData2), annotatedText2); + + // check that going from 1 to 2 is ok + const lineBreakData2from1 = getLineBreakData(factory, tabSize, breakAfter2, 2, wrappingIndent, text, directLineBreakData1); + assert.equal(toAnnotatedText(text, lineBreakData2from1), annotatedText2); + assert.deepEqual(lineBreakData2from1, directLineBreakData2); + + // check that going from 2 to 1 is ok + const lineBreakData1from2 = getLineBreakData(factory, tabSize, breakAfter1, 2, wrappingIndent, text, directLineBreakData2); + assert.equal(toAnnotatedText(text, lineBreakData1from2), annotatedText1); + assert.deepEqual(lineBreakData1from2, directLineBreakData1); + } + + test('MonospaceLineBreaksComputer incremental 1', () => { + + let factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + + assertIncrementalLineBreaks( + factory, 'just some text and more', 4, + 10, 'just some |text and |more', + 15, 'just some text |and more' + ); + + assertIncrementalLineBreaks( + factory, 'Cu scripserit suscipiantur eos, in affert pericula contentiones sed, cetero sanctus et pro. Ius vidit magna regione te, sit ei elaboraret liberavisse. Mundi verear eu mea, eam vero scriptorem in, vix in menandri assueverit. Natum definiebas cu vim. Vim doming vocibus efficiantur id. In indoctum deseruisse voluptatum vim, ad debitis verterem sed.', 4, + 47, 'Cu scripserit suscipiantur eos, in affert |pericula contentiones sed, cetero sanctus et |pro. Ius vidit magna regione te, sit ei |elaboraret liberavisse. Mundi verear eu mea, |eam vero scriptorem in, vix in menandri |assueverit. Natum definiebas cu vim. Vim |doming vocibus efficiantur id. In indoctum |deseruisse voluptatum vim, ad debitis verterem |sed.', + 142, 'Cu scripserit suscipiantur eos, in affert pericula contentiones sed, cetero sanctus et pro. Ius vidit magna regione te, sit ei elaboraret |liberavisse. Mundi verear eu mea, eam vero scriptorem in, vix in menandri assueverit. Natum definiebas cu vim. Vim doming vocibus efficiantur |id. In indoctum deseruisse voluptatum vim, ad debitis verterem sed.', + ); + + assertIncrementalLineBreaks( + factory, 'An his legere persecuti, oblique delicata efficiantur ex vix, vel at graecis officiis maluisset. Et per impedit voluptua, usu discere maiorum at. Ut assum ornatus temporibus vis, an sea melius pericula. Ea dicunt oblique phaedrum nam, eu duo movet nobis. His melius facilis eu, vim malorum temporibus ne. Nec no sale regione, meliore civibus placerat id eam. Mea alii fabulas definitionem te, agam volutpat ad vis, et per bonorum nonumes repudiandae.', 4, + 57, 'An his legere persecuti, oblique delicata efficiantur ex |vix, vel at graecis officiis maluisset. Et per impedit |voluptua, usu discere maiorum at. Ut assum ornatus |temporibus vis, an sea melius pericula. Ea dicunt |oblique phaedrum nam, eu duo movet nobis. His melius |facilis eu, vim malorum temporibus ne. Nec no sale |regione, meliore civibus placerat id eam. Mea alii |fabulas definitionem te, agam volutpat ad vis, et per |bonorum nonumes repudiandae.', + 58, 'An his legere persecuti, oblique delicata efficiantur ex |vix, vel at graecis officiis maluisset. Et per impedit |voluptua, usu discere maiorum at. Ut assum ornatus |temporibus vis, an sea melius pericula. Ea dicunt oblique |phaedrum nam, eu duo movet nobis. His melius facilis eu, |vim malorum temporibus ne. Nec no sale regione, meliore |civibus placerat id eam. Mea alii fabulas definitionem te,| agam volutpat ad vis, et per bonorum nonumes repudiandae.' + ); + + assertIncrementalLineBreaks( + factory, '\t\t"owner": "vscode",', 4, + 14, '\t\t"owner|": |"vscod|e",', + 16, '\t\t"owner":| |"vscode"|,', + WrappingIndent.Same + ); + + assertIncrementalLineBreaks( + factory, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇&👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬', 4, + 51, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇&|👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬', + 50, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇|&|👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬', + WrappingIndent.Same + ); + + assertIncrementalLineBreaks( + factory, '🐇👬&🌞🌖', 4, + 5, '🐇👬&|🌞🌖', + 4, '🐇👬|&|🌞🌖', + WrappingIndent.Same + ); + + assertIncrementalLineBreaks( + factory, '\t\tfunc(\'🌞🏇🍼🌞🏇🍼🐇&👬🌖🌞👬🌖🌞🏇🍼🐇👬\', WrappingIndent.Same);', 4, + 26, '\t\tfunc|(\'🌞🏇🍼🌞🏇🍼🐇&|👬🌖🌞👬🌖🌞🏇🍼🐇|👬\', |WrappingIndent.|Same);', + 27, '\t\tfunc|(\'🌞🏇🍼🌞🏇🍼🐇&|👬🌖🌞👬🌖🌞🏇🍼🐇|👬\', |WrappingIndent.|Same);', + WrappingIndent.Same + ); + + assertIncrementalLineBreaks( + factory, 'factory, "xtxtfunc(x"🌞🏇🍼🌞🏇🍼🐇&👬🌖🌞👬🌖🌞🏇🍼🐇👬x"', 4, + 16, 'factory, |"xtxtfunc|(x"🌞🏇🍼🌞🏇🍼|🐇&|👬🌖🌞👬🌖🌞🏇🍼|🐇👬x"', + 17, 'factory, |"xtxtfunc|(x"🌞🏇🍼🌞🏇🍼🐇|&👬🌖🌞👬🌖🌞🏇🍼|🐇👬x"', + WrappingIndent.Same + ); + }); + + + test('MonospaceLineBreaksComputer - CJK and Kinsoku Shori', () => { + let factory = new MonospaceLineBreaksComputerFactory('(', '\t)'); + assertLineBreaks(factory, 4, 5, 'aa \u5b89|\u5b89'); + assertLineBreaks(factory, 4, 5, '\u3042 \u5b89|\u5b89'); + assertLineBreaks(factory, 4, 5, '\u3042\u3042|\u5b89\u5b89'); + assertLineBreaks(factory, 4, 5, 'aa |\u5b89)\u5b89|\u5b89'); + assertLineBreaks(factory, 4, 5, 'aa \u3042|\u5b89\u3042)|\u5b89'); + assertLineBreaks(factory, 4, 5, 'aa |(\u5b89aa|\u5b89'); + }); + + test('MonospaceLineBreaksComputer - WrappingIndent.Same', () => { + let factory = new MonospaceLineBreaksComputerFactory('', '\t '); + assertLineBreaks(factory, 4, 38, ' *123456789012345678901234567890123456|7890', WrappingIndent.Same); + }); + + test('issue #16332: Scroll bar overlaying on top of text', () => { + let factory = new MonospaceLineBreaksComputerFactory('', '\t '); + assertLineBreaks(factory, 4, 24, 'a/ very/long/line/of/tex|t/that/expands/beyon|d/your/typical/line/|of/code/', WrappingIndent.Indent); + }); + + test('issue #35162: wrappingIndent not consistently working', () => { + let factory = new MonospaceLineBreaksComputerFactory('', '\t '); + let mapper = assertLineBreaks(factory, 4, 24, ' t h i s |i s |a l |o n |g l |i n |e', WrappingIndent.Indent); + assert.equal(mapper!.wrappedTextIndentLength, ' '.length); + }); + + test('issue #75494: surrogate pairs', () => { + let factory = new MonospaceLineBreaksComputerFactory('\t', ' '); + assertLineBreaks(factory, 4, 49, '🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼|🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼🐇👬🌖🌞🏇🍼|🐇👬', WrappingIndent.Same); + }); + + test('issue #75494: surrogate pairs overrun 1', () => { + const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + assertLineBreaks(factory, 4, 4, '🐇👬|&|🌞🌖', WrappingIndent.Same); + }); + + test('issue #75494: surrogate pairs overrun 2', () => { + const factory = new MonospaceLineBreaksComputerFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + assertLineBreaks(factory, 4, 17, 'factory, |"xtxtfunc|(x"🌞🏇🍼🌞🏇🍼🐇|&👬🌖🌞👬🌖🌞🏇🍼|🐇👬x"', WrappingIndent.Same); + }); + + test('MonospaceLineBreaksComputer - WrappingIndent.DeepIndent', () => { + let factory = new MonospaceLineBreaksComputerFactory('', '\t '); + let mapper = assertLineBreaks(factory, 4, 26, ' W e A r e T e s t |i n g D e |e p I n d |e n t a t |i o n', WrappingIndent.DeepIndent); + assert.equal(mapper!.wrappedTextIndentLength, ' '.length); + }); +}); diff --git a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts index 313c672b1016553d3c018f52f65b1da3996b685c..65cc26efd4c12a807c93333f84ccafd2a3848ed7 100644 --- a/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/prefixSumComputer.test.ts @@ -4,9 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { toUint32Array } from 'vs/base/common/uint'; +import { toUint32 } from 'vs/base/common/uint'; import { PrefixSumComputer, PrefixSumIndexOfResult } from 'vs/editor/common/viewModel/prefixSumComputer'; +function toUint32Array(arr: number[]): Uint32Array { + const len = arr.length; + const r = new Uint32Array(len); + for (let i = 0; i < len; i++) { + r[i] = toUint32(arr[i]); + } + return r; +} + suite('Editor ViewModel - PrefixSumComputer', () => { test('PrefixSumComputer', () => { diff --git a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts index a8b64c4da7a19983f1fafb4dcff0ed3722870230..7209c90e881c090726a7c4232634a885ea954d39 100644 --- a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts +++ b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts @@ -9,14 +9,12 @@ import { IViewLineTokens } from 'vs/editor/common/core/lineTokens'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; -import { toUint32Array } from 'vs/base/common/uint'; import { EndOfLinePreference } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import * as modes from 'vs/editor/common/modes'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; -import { CharacterHardWrappingLineMapperFactory, CharacterHardWrappingLineMapping } from 'vs/editor/common/viewModel/characterHardWrappingLineMapper'; -import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { ILineMapping, ISimpleModel, SplitLine, SplitLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; +import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; +import { LineBreakData, ISimpleModel, SplitLine, SplitLinesCollection } from 'vs/editor/common/viewModel/splitLinesCollection'; import { ViewLineData } from 'vs/editor/common/viewModel/viewModel'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -25,7 +23,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; suite('Editor ViewModel - SplitLinesCollection', () => { test('SplitLine', () => { let model1 = createModel('My First LineMy Second LineAnd another one'); - let line1 = createSplitLine([13, 14, 15], ''); + let line1 = createSplitLine([13, 14, 15], [13, 13 + 14, 13 + 14 + 15], 0); assert.equal(line1.getViewLineCount(), 3); assert.equal(line1.getViewLineContent(model1, 1, 0), 'My First Line'); @@ -54,38 +52,38 @@ suite('Editor ViewModel - SplitLinesCollection', () => { } model1 = createModel('My First LineMy Second LineAnd another one'); - line1 = createSplitLine([13, 14, 15], '\t'); + line1 = createSplitLine([13, 14, 15], [13, 13 + 14, 13 + 14 + 15], 4); assert.equal(line1.getViewLineCount(), 3); assert.equal(line1.getViewLineContent(model1, 1, 0), 'My First Line'); - assert.equal(line1.getViewLineContent(model1, 1, 1), '\tMy Second Line'); - assert.equal(line1.getViewLineContent(model1, 1, 2), '\tAnd another one'); + assert.equal(line1.getViewLineContent(model1, 1, 1), ' My Second Line'); + assert.equal(line1.getViewLineContent(model1, 1, 2), ' And another one'); assert.equal(line1.getViewLineMaxColumn(model1, 1, 0), 14); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 1), 16); - assert.equal(line1.getViewLineMaxColumn(model1, 1, 2), 17); - for (let col = 1; col <= 14; col++) { - assert.equal(line1.getModelColumnOfViewPosition(0, col), col, 'getInputColumnOfOutputPosition(0, ' + col + ')'); - } - for (let col = 1; col <= 1; col++) { - assert.equal(line1.getModelColumnOfViewPosition(1, 1), 13 + col, 'getInputColumnOfOutputPosition(1, ' + col + ')'); - } - for (let col = 2; col <= 16; col++) { - assert.equal(line1.getModelColumnOfViewPosition(1, col), 13 + col - 1, 'getInputColumnOfOutputPosition(1, ' + col + ')'); - } - for (let col = 1; col <= 1; col++) { - assert.equal(line1.getModelColumnOfViewPosition(2, col), 13 + 14 + col, 'getInputColumnOfOutputPosition(2, ' + col + ')'); - } - for (let col = 2; col <= 17; col++) { - assert.equal(line1.getModelColumnOfViewPosition(2, col), 13 + 14 + col - 1, 'getInputColumnOfOutputPosition(2, ' + col + ')'); + assert.equal(line1.getViewLineMaxColumn(model1, 1, 1), 19); + assert.equal(line1.getViewLineMaxColumn(model1, 1, 2), 20); + + let actualViewColumnMapping: number[][] = []; + for (let lineIndex = 0; lineIndex < line1.getViewLineCount(); lineIndex++) { + let actualLineViewColumnMapping: number[] = []; + for (let col = 1; col <= line1.getViewLineMaxColumn(model1, 1, lineIndex); col++) { + actualLineViewColumnMapping.push(line1.getModelColumnOfViewPosition(lineIndex, col)); + } + actualViewColumnMapping.push(actualLineViewColumnMapping); } + assert.deepEqual(actualViewColumnMapping, [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + [14, 14, 14, 14, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], + [28, 28, 28, 28, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43], + ]); + for (let col = 1; col <= 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(0, col), '6.getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13; col <= 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, 1 + col - 13), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(1, 4 + col - 13), '7.getOutputPositionOfInputPosition(' + col + ')'); } for (let col = 1 + 13 + 14; col <= 15 + 14 + 13; col++) { - assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, 1 + col - 13 - 14), 'getOutputPositionOfInputPosition(' + col + ')'); + assert.deepEqual(line1.getViewPositionOfModelPosition(0, col), pos(2, 4 + col - 13 - 14), '8.getOutputPositionOfInputPosition(' + col + ')'); } }); @@ -95,13 +93,11 @@ suite('Editor ViewModel - SplitLinesCollection', () => { const fontInfo = config.options.get(EditorOption.fontInfo); const wordWrapBreakAfterCharacters = config.options.get(EditorOption.wordWrapBreakAfterCharacters); const wordWrapBreakBeforeCharacters = config.options.get(EditorOption.wordWrapBreakBeforeCharacters); - const wordWrapBreakObtrusiveCharacters = config.options.get(EditorOption.wordWrapBreakObtrusiveCharacters); const wrappingIndent = config.options.get(EditorOption.wrappingIndent); - const hardWrappingLineMapperFactory = new CharacterHardWrappingLineMapperFactory( + const lineBreaksComputerFactory = new MonospaceLineBreaksComputerFactory( wordWrapBreakBeforeCharacters, - wordWrapBreakAfterCharacters, - wordWrapBreakObtrusiveCharacters + wordWrapBreakAfterCharacters ); const model = TextModel.createFromString([ @@ -115,7 +111,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => { const linesCollection = new SplitLinesCollection( model, - hardWrappingLineMapperFactory, + lineBreaksComputerFactory, model.getOptions().tabSize, wrappingInfo.wrappingColumn, fontInfo.typicalFullwidthCharacterWidth / fontInfo.typicalHalfwidthCharacterWidth, @@ -616,12 +612,12 @@ suite('SplitLinesCollection', () => { ] }, { - content: ' world");', - minColumn: 4, - maxColumn: 12, + content: ' world");', + minColumn: 13, + maxColumn: 21, tokens: [ - { endIndex: 9, value: 15 }, - { endIndex: 11, value: 16 }, + { endIndex: 18, value: 15 }, + { endIndex: 20, value: 16 }, ] }, { @@ -658,28 +654,28 @@ suite('SplitLinesCollection', () => { ] }, { - content: ' world, this is a ', - minColumn: 4, - maxColumn: 21, + content: ' world, this is a ', + minColumn: 13, + maxColumn: 30, tokens: [ - { endIndex: 20, value: 28 }, + { endIndex: 29, value: 28 }, ] }, { - content: ' somewhat longer ', - minColumn: 4, - maxColumn: 20, + content: ' somewhat longer ', + minColumn: 13, + maxColumn: 29, tokens: [ - { endIndex: 19, value: 28 }, + { endIndex: 28, value: 28 }, ] }, { - content: ' line");', - minColumn: 4, - maxColumn: 11, + content: ' line");', + minColumn: 13, + maxColumn: 20, tokens: [ - { endIndex: 8, value: 28 }, - { endIndex: 10, value: 29 }, + { endIndex: 17, value: 28 }, + { endIndex: 19, value: 29 }, ] }, { @@ -749,13 +745,11 @@ suite('SplitLinesCollection', () => { const fontInfo = configuration.options.get(EditorOption.fontInfo); const wordWrapBreakAfterCharacters = configuration.options.get(EditorOption.wordWrapBreakAfterCharacters); const wordWrapBreakBeforeCharacters = configuration.options.get(EditorOption.wordWrapBreakBeforeCharacters); - const wordWrapBreakObtrusiveCharacters = configuration.options.get(EditorOption.wordWrapBreakObtrusiveCharacters); const wrappingIndent = configuration.options.get(EditorOption.wrappingIndent); - const factory = new CharacterHardWrappingLineMapperFactory( + const factory = new MonospaceLineBreaksComputerFactory( wordWrapBreakBeforeCharacters, - wordWrapBreakAfterCharacters, - wordWrapBreakObtrusiveCharacters + wordWrapBreakAfterCharacters ); const linesCollection = new SplitLinesCollection( @@ -778,15 +772,16 @@ function pos(lineNumber: number, column: number): Position { return new Position(lineNumber, column); } -function createSplitLine(splitLengths: number[], wrappedLinesPrefix: string, isVisible: boolean = true): SplitLine { - return new SplitLine(createLineMapping(splitLengths, wrappedLinesPrefix), isVisible); +function createSplitLine(splitLengths: number[], breakingOffsetsVisibleColumn: number[], wrappedTextIndentWidth: number, isVisible: boolean = true): SplitLine { + return new SplitLine(createLineBreakData(splitLengths, breakingOffsetsVisibleColumn, wrappedTextIndentWidth), isVisible); } -function createLineMapping(breakingLengths: number[], wrappedLinesPrefix: string): ILineMapping { - return new CharacterHardWrappingLineMapping( - new PrefixSumComputer(toUint32Array(breakingLengths)), - wrappedLinesPrefix - ); +function createLineBreakData(breakingLengths: number[], breakingOffsetsVisibleColumn: number[], wrappedTextIndentWidth: number): LineBreakData { + let sums: number[] = []; + for (let i = 0; i < breakingLengths.length; i++) { + sums[i] = (i > 0 ? sums[i - 1] : 0) + breakingLengths[i]; + } + return new LineBreakData(sums, breakingOffsetsVisibleColumn, wrappedTextIndentWidth); } function createModel(text: string): ISimpleModel { diff --git a/src/vs/editor/test/common/viewModel/testViewModel.ts b/src/vs/editor/test/common/viewModel/testViewModel.ts index 75b429dd095bca7a25b6fb17d8cf4ebe674415d1..fffa9f05aa95d6e8b6281ddf1e9100aff778b097 100644 --- a/src/vs/editor/test/common/viewModel/testViewModel.ts +++ b/src/vs/editor/test/common/viewModel/testViewModel.ts @@ -7,6 +7,7 @@ import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { TextModel } from 'vs/editor/common/model/textModel'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; +import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; export function testViewModel(text: string[], options: IEditorOptions, callback: (viewModel: ViewModel, model: TextModel) => void): void { const EDITOR_ID = 1; @@ -15,7 +16,7 @@ export function testViewModel(text: string[], options: IEditorOptions, callback: let model = TextModel.createFromString(text.join('\n')); - let viewModel = new ViewModel(EDITOR_ID, configuration, model, null!); + let viewModel = new ViewModel(EDITOR_ID, configuration, model, MonospaceLineBreaksComputerFactory.create(configuration.options), null!); callback(viewModel, model); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 38597bc5d3b76013617d3d9e9fd797014144579a..ace85852ace2c3721109e8b695998944b191f587 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2709,19 +2709,14 @@ declare namespace monaco.editor { wrappingIndent?: 'none' | 'same' | 'indent' | 'deepIndent'; /** * Configure word wrapping characters. A break will be introduced before these characters. - * Defaults to '{([+'. + * Defaults to '([{‘“〈《「『【〔([{「£¥$£¥++'. */ wordWrapBreakBeforeCharacters?: string; /** * Configure word wrapping characters. A break will be introduced after these characters. - * Defaults to ' \t})]?|&,;'. + * Defaults to ' \t})]?|/&.,;¢°′″‰℃、。。、¢,.:;?!%・・ゝゞヽヾーァィゥェォッャュョヮヵヶぁぃぅぇぉっゃゅょゎゕゖㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ々〻ァィゥェォャュョッー”〉》」』】〕)]}」'. */ wordWrapBreakAfterCharacters?: string; - /** - * Configure word wrapping characters. A break will be introduced after these characters only if no `wordWrapBreakBeforeCharacters` or `wordWrapBreakAfterCharacters` were found. - * Defaults to '.'. - */ - wordWrapBreakObtrusiveCharacters?: string; /** * Performance guard: Stop rendering a line after x characters. * Defaults to 10000. diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts index 2b7a9668bd27898dfebe11ef356fcb2333ea0f6e..c8c3d2d6cb8973d965dc30c073af9978a4bdba31 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts @@ -11,6 +11,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import * as marked from 'vs/base/common/marked/marked'; import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; +import { EndOfLinePreference } from 'vs/editor/common/model'; export class WalkThroughModel extends EditorModel { @@ -111,7 +112,7 @@ export class WalkThroughInput extends EditorInput { return ''; }; - const markdown = ref.object.textEditorModel.getLinesContent().join('\n'); + const markdown = ref.object.textEditorModel.getValue(EndOfLinePreference.LF); marked(markdown, { renderer }); return Promise.all(snippets) diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts index add87f122e07844e65f3da27e498ef25e0d6cb91..2be3aee8874b0ef9746de51ef2d0d4cee825d9b3 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts @@ -39,6 +39,7 @@ import { Dimension, size } from 'vs/base/browser/dom'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { domEvent } from 'vs/base/browser/event'; +import { EndOfLinePreference } from 'vs/editor/common/model'; export const WALK_THROUGH_FOCUS = new RawContextKey('interactivePlaygroundFocus', false); @@ -278,7 +279,7 @@ export class WalkThroughPart extends BaseEditor { return; } - const content = model.main.textEditorModel.getLinesContent().join('\n'); + const content = model.main.textEditorModel.getValue(EndOfLinePreference.LF); if (!strings.endsWith(input.getResource().path, '.md')) { this.content.innerHTML = content; this.updateSizeClasses();