diff --git a/src/vs/editor/common/viewModel/characterHardWrappingLineMapper.ts b/src/vs/editor/common/viewModel/characterHardWrappingLineMapper.ts index d1d9a51306dfe0dc93f60fc188c9cc569b805375..0790c9a644401edb8d7ebc895dce4a4d478e2434 100644 --- a/src/vs/editor/common/viewModel/characterHardWrappingLineMapper.ts +++ b/src/vs/editor/common/viewModel/characterHardWrappingLineMapper.ts @@ -105,6 +105,222 @@ function createLineMapping(classifier: WrappingCharacterClassifier, previousBrea let breakingOffsets: number[] = []; let breakingOffsetsVisibleColumn: number[] = []; let breakingOffsetsCount: number = 0; + + if (previousBreakingData/* && firstLineBreakingColumn >= 10 && Math.abs(previousBreakingData.breakingColumn - firstLineBreakingColumn) <= 3 */) { + const prevBreakingOffsets = previousBreakingData.breakOffsets; + const prevBreakingOffsetsVisibleColumn = previousBreakingData.breakingOffsetsVisibleColumn; + + let breakingColumn = firstLineBreakingColumn; + const prevLen = prevBreakingOffsets.length; + let prevIndex = 0; + while (prevIndex < prevLen) { + + // Allow for prevIndex to be -1 (for the case where we hit a tab when walking backwards from the first break) + let breakOffset = prevIndex < 0 ? 0 : prevBreakingOffsets[prevIndex]; + let breakOffsetVisibleColumn = prevIndex < 0 ? 0 : prevBreakingOffsetsVisibleColumn[prevIndex]; + + if (breakOffsetVisibleColumn === breakingColumn) { + // perfect fit, nothing to do + breakingOffsets[breakingOffsetsCount] = breakOffset; + breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; + breakingOffsetsCount++; + breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakingColumn; + prevIndex++; + } else if (breakOffsetVisibleColumn < breakingColumn) { + // try to add more characters + const initialBreakOffset = breakOffset; + let visibleColumn = breakOffsetVisibleColumn; + breakOffset = 0; + + let prevCharCode = lineText.charCodeAt(initialBreakOffset - 1); + let prevCharCodeClass = classifier.get(prevCharCode); + let mustBreak = false; + for (let i = initialBreakOffset; i < len; i++) { + const charCode = lineText.charCodeAt(i); + const charCodeClass = classifier.get(charCode); + + 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 = charCode; + prevCharCodeClass = charCodeClass; + continue; + } + + if (canBreak(prevCharCodeClass, charCodeClass)) { + breakOffset = i; + breakOffsetVisibleColumn = visibleColumn; + } + + const charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar); + visibleColumn += charWidth; + + if (visibleColumn > breakingColumn) { + // We need to break at least before character at `i`: + + if (breakOffset === 0 || visibleColumn - breakOffsetVisibleColumn > wrappedLineBreakingColumn) { + // Cannot break at `breakOffset`, must break at `i` + breakOffset = i; + breakOffsetVisibleColumn = visibleColumn - charWidth; + } + + mustBreak = true; + break; + } + + prevCharCode = charCode; + prevCharCodeClass = charCodeClass; + } + + if (!mustBreak) { + // there is no more need to break => stop the outer loop! + // Add last segment + breakingOffsets[breakingOffsetsCount] = prevBreakingOffsets[prevBreakingOffsets.length - 1]; + breakingOffsetsVisibleColumn[breakingOffsetsCount] = prevBreakingOffsetsVisibleColumn[prevBreakingOffsets.length - 1]; + break; + } + + breakingOffsets[breakingOffsetsCount] = breakOffset; + breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; + breakingOffsetsCount++; + breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakingColumn; + prevIndex++; + } else if (breakOffsetVisibleColumn > breakingColumn) { + const initialBreakOffset = breakOffset; + let visibleColumn = breakOffsetVisibleColumn; + breakOffset = 0; + + let charCode = lineText.charCodeAt(initialBreakOffset); + let charCodeClass = classifier.get(charCode); + let hitTab = false; + + let firstValidBreakOffset = 0; + let firstValidBreakOffsetVisibleColumn = 0; + for (let i = initialBreakOffset - 1; i >= 0; i--) { + let prevCharCode = lineText.charCodeAt(i); + let prevCharCodeClass = classifier.get(prevCharCode); + + if (strings.isHighSurrogate(prevCharCode)) { + // A surrogate pair must always be considered as a single unit, so it is never to be broken + visibleColumn -= 1; + charCode = prevCharCode; + charCodeClass = prevCharCodeClass; + continue; + } + + if (prevCharCode === CharCode.Tab) { + // cannot determine the width of a tab when going backwards, so we must go forwards + hitTab = true; + break; + } + + const charWidth = (strings.isFullWidthCharacter(prevCharCode) ? columnsForFullWidthChar : 1); + + if (visibleColumn <= breakingColumn) { + if (firstValidBreakOffset === 0) { + firstValidBreakOffset = i + 1; + firstValidBreakOffsetVisibleColumn = visibleColumn; + } + + if (visibleColumn <= breakingColumn - wrappedLineBreakingColumn) { + // went too far! + break; + } + + if (canBreak(prevCharCodeClass, charCodeClass)) { + breakOffset = i + 1; + breakOffsetVisibleColumn = visibleColumn; + break; + } + } + + visibleColumn -= charWidth; + charCode = prevCharCode; + charCodeClass = prevCharCodeClass; + } + + if (hitTab) { + // cannot determine the width of a tab when going backwards, so we must go forwards + prevIndex--; + continue; + } + + if (breakOffset === 0) { + // Could not find a good breaking point + breakOffset = firstValidBreakOffset; + breakOffsetVisibleColumn = firstValidBreakOffsetVisibleColumn; + } + + breakingOffsets[breakingOffsetsCount] = breakOffset; + breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; + breakingOffsetsCount++; + breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakingColumn; + } + + if (prevIndex < 0) { + prevIndex = 0; + } else { + let currentDiff = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex] - breakingColumn); + while (prevIndex + 1 < prevLen) { + const potentialDiff = Math.abs(prevBreakingOffsetsVisibleColumn[prevIndex + 1] - breakingColumn); + if (potentialDiff >= currentDiff) { + break; + } + currentDiff = potentialDiff; + prevIndex++; + } + } + } + + if (breakingOffsetsCount === 0) { + return null; + } + + return new LineBreakingData(firstLineBreakingColumn, breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength); + const expected = createLineMapping(classifier, null, lineText, tabSize, firstLineBreakingColumn, columnsForFullWidthChar, hardWrappingIndent); + const actual = new LineBreakingData(firstLineBreakingColumn, breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength); + try { + actual.assertEqual(expected); + } catch (err) { + console.log(`BREAKING!!`); + console.log(err); + console.log(` + assertIncrementalLineMapping( + factory, ${str(lineText)}, 4, + ${previousBreakingData.breakingColumn}, ${str(toAnnotatedText(lineText, previousBreakingData))}, + ${expected!.breakingColumn}, ${str(toAnnotatedText(lineText, expected))} + ); +`); + function str(strr: string) { + return `'${strr.replace(/'/g, '\\\'')}'`; + } + function toAnnotatedText(text: string, lineBreakingData: LineBreakingData | null): string { + // Insert line break markers again, according to algorithm + let actualAnnotatedText = ''; + if (lineBreakingData) { + let previousLineIndex = 0; + for (let i = 0, len = text.length; i < len; i++) { + let r = LineBreakingData.getOutputPositionOfInputOffset(lineBreakingData.breakOffsets, i); + if (previousLineIndex !== r.outputLineIndex) { + previousLineIndex = r.outputLineIndex; + actualAnnotatedText += '|'; + } + actualAnnotatedText += text.charAt(i); + } + } else { + // No wrapping + actualAnnotatedText = text; + } + return actualAnnotatedText; + } + } + return actual; + + breakingOffsets = []; + breakingOffsetsVisibleColumn = []; + breakingOffsetsCount = 0; + } + let breakOffset = 0; let breakOffsetVisibleColumn = 0; @@ -165,6 +381,62 @@ function createLineMapping(classifier: WrappingCharacterClassifier, previousBrea return new LineBreakingData(firstLineBreakingColumn, breakingOffsets, breakingOffsetsVisibleColumn, wrappedTextIndentLength); } +// class BreakSearchResult { + +// public static INSTANCE = new BreakSearchResult(); + +// prevCharCode: number = CharCode.Null; +// prevCharCodeClass: CharacterClass = CharacterClass.NONE; +// breakOffset: number = 0; +// breakOffsetVisibleColumn: number = 0; +// visibleColumn: number = 0; +// } + +// function searchForBreak(classifier: WrappingCharacterClassifier, lineText: string, len: number, prevCharCode: number, prevCharCodeClass: number): boolean { +// let breakOffset = 0; +// let breakOffsetVisibleColumn = 0; +// for (let i = 1; i < len; i++) { +// const charCode = lineText.charCodeAt(i); +// const charCodeClass = classifier.get(charCode); + +// 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 = charCode; +// prevCharCodeClass = charCodeClass; +// continue; +// } + +// if (canBreak(prevCharCodeClass, charCodeClass)) { +// breakOffset = i; +// breakOffsetVisibleColumn = visibleColumn; +// } + +// const charWidth = computeCharWidth(charCode, visibleColumn, tabSize, columnsForFullWidthChar); +// 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 > wrappedLineBreakingColumn) { +// // Cannot break at `breakOffset`, must break at `i` +// breakOffset = i; +// breakOffsetVisibleColumn = visibleColumn - charWidth; +// } + +// breakingOffsets[breakingOffsetsCount] = breakOffset; +// breakingOffsetsVisibleColumn[breakingOffsetsCount] = breakOffsetVisibleColumn; +// breakingOffsetsCount++; +// breakingColumn = breakOffsetVisibleColumn + wrappedLineBreakingColumn; +// breakOffset = 0; +// } + +// prevCharCode = charCode; +// prevCharCodeClass = charCodeClass; +// } +// } + function computeCharWidth(charCode: number, visibleColumn: number, tabSize: number, columnsForFullWidthChar: number): number { if (charCode === CharCode.Tab) { return (tabSize - (visibleColumn % tabSize)); diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index 6346d5bbc4cfdca258c6ef4a2ed331b06b958ddf..2f0af80ac2455efdd413546453c192801be76927 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -34,6 +34,29 @@ export class LineBreakingData { public readonly wrappedTextIndentLength: number ) { } + assertEqual(other: LineBreakingData | null): void { + if (other === null) { + throw new Error(`x--unexpected--1`); + } + if (other.breakingColumn !== this.breakingColumn) { + throw new Error(`x--unexpected--2`); + } + if (other.wrappedTextIndentLength !== this.wrappedTextIndentLength) { + throw new Error(`x--unexpected--3`); + } + if (other.breakOffsets.length !== this.breakOffsets.length) { + throw new Error(`x--unexpected--4`); + } + for (let i = 0; i < this.breakOffsets.length; i++) { + if (this.breakOffsets[i] !== other.breakOffsets[i]) { + throw new Error(`x--unexpected--5`); + } + if (this.breakingOffsetsVisibleColumn[i] !== other.breakingOffsetsVisibleColumn[i]) { + throw new Error(`x--unexpected--6`); + } + } + } + public static getInputOffsetOfOutputPosition(breakOffsets: number[], outputLineIndex: number, outputOffset: number): number { if (outputLineIndex === 0) { return outputOffset; diff --git a/src/vs/editor/test/common/viewModel/characterHardWrappingLineMapper.test.ts b/src/vs/editor/test/common/viewModel/characterHardWrappingLineMapper.test.ts index a1aec876698a8f799fa628f6e25bbdc17a590706..4890907d5f97186acee7e154e42a483ee3749c81 100644 --- a/src/vs/editor/test/common/viewModel/characterHardWrappingLineMapper.test.ts +++ b/src/vs/editor/test/common/viewModel/characterHardWrappingLineMapper.test.ts @@ -3,49 +3,60 @@ * 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 { WrappingIndent, EditorOptions } from 'vs/editor/common/config/editorOptions'; import { CharacterHardWrappingLineMapperFactory } from 'vs/editor/common/viewModel/characterHardWrappingLineMapper'; import { ILineMapperFactory, LineBreakingData } from 'vs/editor/common/viewModel/splitLinesCollection'; -function assertLineMapping(factory: ILineMapperFactory, tabSize: number, breakAfter: number, annotatedText: string, wrappingIndent = WrappingIndent.None): LineBreakingData | null { - // Create version of `annotatedText` with line break markers removed - let rawText = ''; +function parseAnnotatedText(annotatedText: string): { text: string; indices: number[]; } { + let text = ''; let currentLineIndex = 0; - let lineIndices: number[] = []; + let indices: 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; + text += annotatedText.charAt(i); + indices[text.length - 1] = currentLineIndex; } } + return { text: text, indices: indices }; +} - const lineMappingComputer = factory.createLineMappingComputer(tabSize, breakAfter, 2, wrappingIndent); - lineMappingComputer.addRequest(rawText, null); - const lineMappings = lineMappingComputer.finalize(); - const mapper = lineMappings[0]; - +function toAnnotatedText(text: string, lineBreakingData: LineBreakingData | null): string { // Insert line break markers again, according to algorithm let actualAnnotatedText = ''; - if (mapper) { + if (lineBreakingData) { let previousLineIndex = 0; - for (let i = 0, len = rawText.length; i < len; i++) { - let r = LineBreakingData.getOutputPositionOfInputOffset(mapper.breakOffsets, i); + for (let i = 0, len = text.length; i < len; i++) { + let r = LineBreakingData.getOutputPositionOfInputOffset(lineBreakingData.breakOffsets, i); if (previousLineIndex !== r.outputLineIndex) { previousLineIndex = r.outputLineIndex; actualAnnotatedText += '|'; } - actualAnnotatedText += rawText.charAt(i); + actualAnnotatedText += text.charAt(i); } } else { // No wrapping - actualAnnotatedText = rawText; + actualAnnotatedText = text; } + return actualAnnotatedText; +} + +function getLineBreakingData(factory: ILineMapperFactory, tabSize: number, breakAfter: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, text: string, previousLineBreakingData: LineBreakingData | null): LineBreakingData | null { + const lineMappingComputer = factory.createLineMappingComputer(tabSize, breakAfter, columnsForFullWidthChar, wrappingIndent); + lineMappingComputer.addRequest(text, previousLineBreakingData); + return lineMappingComputer.finalize()[0]; +} + +function assertLineMapping(factory: ILineMapperFactory, tabSize: number, breakAfter: number, annotatedText: string, wrappingIndent = WrappingIndent.None): LineBreakingData | null { + // Create version of `annotatedText` with line break markers removed + const text = parseAnnotatedText(annotatedText).text; + const lineBreakingData = getLineBreakingData(factory, tabSize, breakAfter, 2, wrappingIndent, text, null); + const actualAnnotatedText = toAnnotatedText(text, lineBreakingData); assert.equal(actualAnnotatedText, annotatedText); - return mapper; + return lineBreakingData; } suite('Editor ViewModel - CharacterHardWrappingLineMapper', () => { @@ -91,6 +102,54 @@ suite('Editor ViewModel - CharacterHardWrappingLineMapper', () => { assertLineMapping(factory, 4, 5, 'aa.(.|).aaa'); }); + function assertIncrementalLineMapping(factory: ILineMapperFactory, 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 directLineBreakingData1 = getLineBreakingData(factory, tabSize, breakAfter1, 2, wrappingIndent, text, null); + assert.equal(toAnnotatedText(text, directLineBreakingData1), annotatedText1); + + // check that the direct mapping is ok for 2 + const directLineBreakingData2 = getLineBreakingData(factory, tabSize, breakAfter2, 2, wrappingIndent, text, null); + assert.equal(toAnnotatedText(text, directLineBreakingData2), annotatedText2); + + // check that going from 1 to 2 is ok + const lineBreakingData2from1 = getLineBreakingData(factory, tabSize, breakAfter2, 2, wrappingIndent, text, directLineBreakingData1); + assert.equal(toAnnotatedText(text, lineBreakingData2from1), annotatedText2); + assert.deepEqual(lineBreakingData2from1, directLineBreakingData2); + + // check that going from 2 to 1 is ok + const lineBreakingData1from2 = getLineBreakingData(factory, tabSize, breakAfter1, 2, wrappingIndent, text, directLineBreakingData2); + assert.equal(toAnnotatedText(text, lineBreakingData1from2), annotatedText1); + assert.deepEqual(lineBreakingData1from2, directLineBreakingData1); + } + + test('CharacterHardWrappingLineMapper incremental 1', () => { + + let factory = new CharacterHardWrappingLineMapperFactory(EditorOptions.wordWrapBreakBeforeCharacters.defaultValue, EditorOptions.wordWrapBreakAfterCharacters.defaultValue); + + assertIncrementalLineMapping( + factory, 'just some text and more', 4, + 10, 'just some |text and |more', + 15, 'just some text |and more' + ); + + assertIncrementalLineMapping( + 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.', + ); + + assertIncrementalLineMapping( + 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.' + ); + }); + + test('CharacterHardWrappingLineMapper - CJK and Kinsoku Shori', () => { let factory = new CharacterHardWrappingLineMapperFactory('(', '\t)'); assertLineMapping(factory, 4, 5, 'aa \u5b89|\u5b89');