From 8de25f94a57c81f4eacbae4173c0bb00a526c7fe Mon Sep 17 00:00:00 2001 From: Griffon Langyer <146424306+rfon6ngy@users.noreply.github.com> Date: Mon, 14 Jul 2025 14:41:26 +0200 Subject: [PATCH] Merge pull request #231120 from rfon6ngy/patch-1 Allow \n to trigger a softwrap --- .../browser/view/domLineBreaksComputer.ts | 2 +- src/vs/editor/common/config/editorOptions.ts | 10 ++++++ .../editor/common/modelLineProjectionData.ts | 2 +- .../common/standalone/standaloneEnums.ts | 25 ++++++++------- .../viewModel/monospaceLineBreaksComputer.ts | 18 +++++++++-- .../editor/common/viewModel/viewModelImpl.ts | 6 ++-- .../editor/common/viewModel/viewModelLines.ts | 7 +++-- .../viewModel/modelLineProjection.test.ts | 15 +++++---- .../monospaceLineBreaksComputer.test.ts | 14 ++++----- src/vs/monaco.d.ts | 31 ++++++++++++------- 10 files changed, 84 insertions(+), 46 deletions(-) diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index b2efc74cf9f..6eed0a076be 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -26,7 +26,7 @@ export class DOMLineBreaksComputerFactory implements ILineBreaksComputerFactory constructor(private targetWindow: WeakRef) { } - public createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll'): ILineBreaksComputer { + public createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ILineBreaksComputer { const requests: string[] = []; const injectedTexts: (LineInjectedText[] | null)[] = []; return { diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 07c2aadf3ce..a4e6b9cf316 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -357,6 +357,11 @@ export interface IEditorOptions { * Defaults to 'simple'. */ wrappingStrategy?: 'simple' | 'advanced'; + /** + * Create a softwrap on every quoted "\n" literal. + * Defaults to false. + */ + wrapOnEscapedLineFeeds?: boolean; /** * Configure word wrapping characters. A break will be introduced before these characters. */ @@ -5712,6 +5717,7 @@ export const enum EditorOption { showDeprecated, inertialScroll, inlayHints, + wrapOnEscapedLineFeeds, // Leave these at the end (because they have dependencies!) effectiveCursorStyle, editorClassName, @@ -6568,6 +6574,10 @@ export const EditorOptions = { 'inherit' as 'off' | 'on' | 'inherit', ['off', 'on', 'inherit'] as const )), + wrapOnEscapedLineFeeds: register(new EditorBooleanOption( + EditorOption.wrapOnEscapedLineFeeds, 'wrapOnEscapedLineFeeds', false, + { markdownDescription: nls.localize('wrapOnEscapedLineFeeds', "Controls whether literal `\\n` shall trigger a wordWrap.\nfor example\n```c\nchar* str=\"hello\\nworld\"\n```\nwill be displayed as\n```c\nchar* str=\"hello\\n\n world\"\n```") } + )), // Leave these at the end (because they have dependencies!) effectiveCursorStyle: register(new EffectiveCursorStyle()), diff --git a/src/vs/editor/common/modelLineProjectionData.ts b/src/vs/editor/common/modelLineProjectionData.ts index f35a4adc6e5..aac6ae4642d 100644 --- a/src/vs/editor/common/modelLineProjectionData.ts +++ b/src/vs/editor/common/modelLineProjectionData.ts @@ -329,7 +329,7 @@ export class OutputPosition { } export interface ILineBreaksComputerFactory { - createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll'): ILineBreaksComputer; + createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ILineBreaksComputer; } export interface ILineBreaksComputer { diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 154fb8b9c17..7af7784ad14 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -327,18 +327,19 @@ export enum EditorOption { showDeprecated = 150, inertialScroll = 151, inlayHints = 152, - effectiveCursorStyle = 153, - editorClassName = 154, - pixelRatio = 155, - tabFocusMode = 156, - layoutInfo = 157, - wrappingInfo = 158, - defaultColorDecorators = 159, - colorDecoratorsActivatedOn = 160, - inlineCompletionsAccessibilityVerbose = 161, - effectiveEditContext = 162, - scrollOnMiddleClick = 163, - effectiveAllowVariableFonts = 164 + wrapOnEscapedLineFeeds = 153, + effectiveCursorStyle = 154, + editorClassName = 155, + pixelRatio = 156, + tabFocusMode = 157, + layoutInfo = 158, + wrappingInfo = 159, + defaultColorDecorators = 160, + colorDecoratorsActivatedOn = 161, + inlineCompletionsAccessibilityVerbose = 162, + effectiveEditContext = 163, + scrollOnMiddleClick = 164, + effectiveAllowVariableFonts = 165 } /** diff --git a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts index 434e322b6f0..00632b25780 100644 --- a/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts +++ b/src/vs/editor/common/viewModel/monospaceLineBreaksComputer.ts @@ -26,7 +26,7 @@ export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFa this.classifier = new WrappingCharacterClassifier(breakBeforeChars, breakAfterChars); } - public createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll'): ILineBreaksComputer { + public createLineBreaksComputer(fontInfo: FontInfo, tabSize: number, wrappingColumn: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ILineBreaksComputer { const requests: string[] = []; const injectedTexts: (LineInjectedText[] | null)[] = []; const previousBreakingData: (ModelLineProjectionData | null)[] = []; @@ -45,7 +45,7 @@ export class MonospaceLineBreaksComputerFactory implements ILineBreaksComputerFa if (previousLineBreakData && !previousLineBreakData.injectionOptions && !injectedText) { result[i] = createLineBreaksFromPreviousLineBreaks(this.classifier, previousLineBreakData, requests[i], tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak); } else { - result[i] = createLineBreaks(this.classifier, requests[i], injectedText, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak); + result[i] = createLineBreaks(this.classifier, requests[i], injectedText, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent, wordBreak, wrapOnEscapedLineFeeds); } } arrPool1.length = 0; @@ -355,7 +355,7 @@ function createLineBreaksFromPreviousLineBreaks(classifier: WrappingCharacterCla return previousBreakingData; } -function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: string, injectedTexts: LineInjectedText[] | null, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll'): ModelLineProjectionData | null { +function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: string, injectedTexts: LineInjectedText[] | null, tabSize: number, firstLineBreakColumn: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean): ModelLineProjectionData | null { const lineText = LineInjectedText.applyInjectedText(_lineText, injectedTexts); let injectionOptions: InjectedTextOptions[] | null; @@ -434,6 +434,18 @@ function createLineBreaks(classifier: WrappingCharacterClassifier, _lineText: st visibleColumn += charWidth; + // literal \n shall trigger a softwrap + if ( + wrapOnEscapedLineFeeds + && i >= 2 + && (i < 3 || lineText.charAt(i - 3) !== '\\') + && lineText.charAt(i - 2) === '\\' + && lineText.charAt(i - 1) === 'n' + && lineText.includes('"') + ) { + visibleColumn += breakingColumn; + } + // 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`: diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index dd4f4602edb..5213ac45eed 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -101,6 +101,7 @@ export class ViewModel extends Disposable implements IViewModel { const wrappingInfo = options.get(EditorOption.wrappingInfo); const wrappingIndent = options.get(EditorOption.wrappingIndent); const wordBreak = options.get(EditorOption.wordBreak); + const wrapOnEscapedLineFeeds = options.get(EditorOption.wrapOnEscapedLineFeeds); this._lines = new ViewModelLinesFromProjectedModel( this._editorId, @@ -112,7 +113,8 @@ export class ViewModel extends Disposable implements IViewModel { wrappingStrategy, wrappingInfo.wrappingColumn, wrappingIndent, - wordBreak + wordBreak, + wrapOnEscapedLineFeeds ); } @@ -1337,7 +1339,7 @@ class HiddenAreasModel { } function mergeLineRangeArray(arr1: Range[], arr2: Range[]): Range[] { - const result = []; + const result: Range[] = []; let i = 0; let j = 0; while (i < arr1.length && j < arr2.length) { diff --git a/src/vs/editor/common/viewModel/viewModelLines.ts b/src/vs/editor/common/viewModel/viewModelLines.ts index 74eef52bafa..8d76ec87fc6 100644 --- a/src/vs/editor/common/viewModel/viewModelLines.ts +++ b/src/vs/editor/common/viewModel/viewModelLines.ts @@ -72,6 +72,7 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { private wrappingIndent: WrappingIndent; private wordBreak: 'normal' | 'keepAll'; private wrappingStrategy: 'simple' | 'advanced'; + private wrapOnEscapedLineFeeds: boolean; private modelLineProjections!: IModelLineProjection[]; @@ -92,7 +93,8 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { wrappingStrategy: 'simple' | 'advanced', wrappingColumn: number, wrappingIndent: WrappingIndent, - wordBreak: 'normal' | 'keepAll' + wordBreak: 'normal' | 'keepAll', + wrapOnEscapedLineFeeds: boolean ) { this._editorId = editorId; this.model = model; @@ -105,6 +107,7 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { this.wrappingColumn = wrappingColumn; this.wrappingIndent = wrappingIndent; this.wordBreak = wordBreak; + this.wrapOnEscapedLineFeeds = wrapOnEscapedLineFeeds; this._constructLines(/*resetHiddenAreas*/true, null); } @@ -310,7 +313,7 @@ export class ViewModelLinesFromProjectedModel implements IViewModelLines { ? this._domLineBreaksComputerFactory : this._monospaceLineBreaksComputerFactory ); - return lineBreaksComputerFactory.createLineBreaksComputer(this.fontInfo, this.tabSize, this.wrappingColumn, this.wrappingIndent, this.wordBreak); + return lineBreaksComputerFactory.createLineBreaksComputer(this.fontInfo, this.tabSize, this.wrappingColumn, this.wrappingIndent, this.wordBreak, this.wrapOnEscapedLineFeeds); } public onModelFlushed(): void { diff --git a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts index 417249f546b..fbf899499eb 100644 --- a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts +++ b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts @@ -101,6 +101,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => { const wordWrapBreakBeforeCharacters = config.options.get(EditorOption.wordWrapBreakBeforeCharacters); const wrappingIndent = config.options.get(EditorOption.wrappingIndent); const wordBreak = config.options.get(EditorOption.wordBreak); + const wrapOnEscapedLineFeeds = config.options.get(EditorOption.wrapOnEscapedLineFeeds); const lineBreaksComputerFactory = new MonospaceLineBreaksComputerFactory(wordWrapBreakBeforeCharacters, wordWrapBreakAfterCharacters); const model = createTextModel([ @@ -122,7 +123,8 @@ suite('Editor ViewModel - SplitLinesCollection', () => { 'simple', wrappingInfo.wrappingColumn, wrappingIndent, - wordBreak + wordBreak, + wrapOnEscapedLineFeeds ); callback(model, linesCollection); @@ -439,7 +441,7 @@ suite('SplitLinesCollection', () => { } test('getViewLinesData - no wrapping', () => { - withSplitLinesCollection(model, 'off', 0, (splitLinesCollection) => { + withSplitLinesCollection(model, 'off', 0, false, (splitLinesCollection) => { assert.strictEqual(splitLinesCollection.getViewLineCount(), 8); assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), true); @@ -573,7 +575,7 @@ suite('SplitLinesCollection', () => { }); test('getViewLinesData - with wrapping', () => { - withSplitLinesCollection(model, 'wordWrapColumn', 30, (splitLinesCollection) => { + withSplitLinesCollection(model, 'wordWrapColumn', 30, false, (splitLinesCollection) => { assert.strictEqual(splitLinesCollection.getViewLineCount(), 12); assert.strictEqual(splitLinesCollection.modelPositionIsVisible(1, 1), true); assert.strictEqual(splitLinesCollection.modelPositionIsVisible(2, 1), true); @@ -758,7 +760,7 @@ suite('SplitLinesCollection', () => { } }]); - withSplitLinesCollection(model, 'wordWrapColumn', 30, (splitLinesCollection) => { + withSplitLinesCollection(model, 'wordWrapColumn', 30, false, (splitLinesCollection) => { assert.strictEqual(splitLinesCollection.getViewLineCount(), 14); assert.strictEqual(splitLinesCollection.getViewLineMaxColumn(1), 24); @@ -944,7 +946,7 @@ suite('SplitLinesCollection', () => { }); }); - function withSplitLinesCollection(model: TextModel, wordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded', wordWrapColumn: number, callback: (splitLinesCollection: ViewModelLinesFromProjectedModel) => void): void { + function withSplitLinesCollection(model: TextModel, wordWrap: 'on' | 'off' | 'wordWrapColumn' | 'bounded', wordWrapColumn: number, wrapOnEscapedLineFeeds: boolean, callback: (splitLinesCollection: ViewModelLinesFromProjectedModel) => void): void { const configuration = new TestConfiguration({ wordWrap: wordWrap, wordWrapColumn: wordWrapColumn, @@ -969,7 +971,8 @@ suite('SplitLinesCollection', () => { 'simple', wrappingInfo.wrappingColumn, wrappingIndent, - wordBreak + wordBreak, + wrapOnEscapedLineFeeds ); callback(linesCollection); diff --git a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts index b5f0e782224..850e19bc4ad 100644 --- a/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts +++ b/src/vs/editor/test/common/viewModel/monospaceLineBreaksComputer.test.ts @@ -44,7 +44,7 @@ function toAnnotatedText(text: string, lineBreakData: ModelLineProjectionData | return actualAnnotatedText; } -function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', text: string, previousLineBreakData: ModelLineProjectionData | null): ModelLineProjectionData | null { +function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, columnsForFullWidthChar: number, wrappingIndent: WrappingIndent, wordBreak: 'normal' | 'keepAll', wrapOnEscapedLineFeeds: boolean, text: string, previousLineBreakData: ModelLineProjectionData | null): ModelLineProjectionData | null { const fontInfo = new FontInfo({ pixelRatio: 1, fontFamily: 'testFontFamily', @@ -63,7 +63,7 @@ function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, wsmiddotWidth: 7, maxDigitWidth: 7 }, false); - const lineBreaksComputer = factory.createLineBreaksComputer(fontInfo, tabSize, breakAfter, wrappingIndent, wordBreak); + const lineBreaksComputer = factory.createLineBreaksComputer(fontInfo, tabSize, breakAfter, wrappingIndent, wordBreak, wrapOnEscapedLineFeeds); const previousLineBreakDataClone = previousLineBreakData ? new ModelLineProjectionData(null, null, previousLineBreakData.breakOffsets.slice(0), previousLineBreakData.breakOffsetsVisibleColumn.slice(0), previousLineBreakData.wrappedTextIndentLength) : null; lineBreaksComputer.addRequest(text, null, previousLineBreakDataClone); return lineBreaksComputer.finalize()[0]; @@ -72,7 +72,7 @@ function getLineBreakData(factory: ILineBreaksComputerFactory, tabSize: number, function assertLineBreaks(factory: ILineBreaksComputerFactory, tabSize: number, breakAfter: number, annotatedText: string, wrappingIndent = WrappingIndent.None, wordBreak: 'normal' | 'keepAll' = 'normal'): ModelLineProjectionData | null { // Create version of `annotatedText` with line break markers removed const text = parseAnnotatedText(annotatedText).text; - const lineBreakData = getLineBreakData(factory, tabSize, breakAfter, 2, wrappingIndent, wordBreak, text, null); + const lineBreakData = getLineBreakData(factory, tabSize, breakAfter, 2, wrappingIndent, wordBreak, false, text, null); const actualAnnotatedText = toAnnotatedText(text, lineBreakData); assert.strictEqual(actualAnnotatedText, annotatedText); @@ -145,20 +145,20 @@ suite('Editor ViewModel - MonospaceLineBreaksComputer', () => { assert.strictEqual(text, parseAnnotatedText(annotatedText2).text); // check that the direct mapping is ok for 1 - const directLineBreakData1 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, 'normal', text, null); + const directLineBreakData1 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, 'normal', false, text, null); assert.strictEqual(toAnnotatedText(text, directLineBreakData1), annotatedText1); // check that the direct mapping is ok for 2 - const directLineBreakData2 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, 'normal', text, null); + const directLineBreakData2 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, 'normal', false, text, null); assert.strictEqual(toAnnotatedText(text, directLineBreakData2), annotatedText2); // check that going from 1 to 2 is ok - const lineBreakData2from1 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, 'normal', text, directLineBreakData1); + const lineBreakData2from1 = getLineBreakData(factory, tabSize, breakAfter2, columnsForFullWidthChar, wrappingIndent, 'normal', false, text, directLineBreakData1); assert.strictEqual(toAnnotatedText(text, lineBreakData2from1), annotatedText2); assertLineBreakDataEqual(lineBreakData2from1, directLineBreakData2); // check that going from 2 to 1 is ok - const lineBreakData1from2 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, 'normal', text, directLineBreakData2); + const lineBreakData1from2 = getLineBreakData(factory, tabSize, breakAfter1, columnsForFullWidthChar, wrappingIndent, 'normal', false, text, directLineBreakData2); assert.strictEqual(toAnnotatedText(text, lineBreakData1from2), annotatedText1); assertLineBreakDataEqual(lineBreakData1from2, directLineBreakData1); } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 62054c90e74..0a88f8db3de 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3478,6 +3478,11 @@ declare namespace monaco.editor { * Defaults to 'simple'. */ wrappingStrategy?: 'simple' | 'advanced'; + /** + * Create a softwrap on every quoted "\n" literal. + * Defaults to false. + */ + wrapOnEscapedLineFeeds?: boolean; /** * Configure word wrapping characters. A break will be introduced before these characters. */ @@ -5148,18 +5153,19 @@ declare namespace monaco.editor { showDeprecated = 150, inertialScroll = 151, inlayHints = 152, - effectiveCursorStyle = 153, - editorClassName = 154, - pixelRatio = 155, - tabFocusMode = 156, - layoutInfo = 157, - wrappingInfo = 158, - defaultColorDecorators = 159, - colorDecoratorsActivatedOn = 160, - inlineCompletionsAccessibilityVerbose = 161, - effectiveEditContext = 162, - scrollOnMiddleClick = 163, - effectiveAllowVariableFonts = 164 + wrapOnEscapedLineFeeds = 153, + effectiveCursorStyle = 154, + editorClassName = 155, + pixelRatio = 156, + tabFocusMode = 157, + layoutInfo = 158, + wrappingInfo = 159, + defaultColorDecorators = 160, + colorDecoratorsActivatedOn = 161, + inlineCompletionsAccessibilityVerbose = 162, + effectiveEditContext = 163, + scrollOnMiddleClick = 164, + effectiveAllowVariableFonts = 165 } export const EditorOptions: { @@ -5317,6 +5323,7 @@ declare namespace monaco.editor { wordWrapColumn: IEditorOption; wordWrapOverride1: IEditorOption; wordWrapOverride2: IEditorOption; + wrapOnEscapedLineFeeds: IEditorOption; effectiveCursorStyle: IEditorOption; editorClassName: IEditorOption; defaultColorDecorators: IEditorOption;