Merge pull request #231120 from rfon6ngy/patch-1

Allow \n to trigger a softwrap
This commit is contained in:
Griffon Langyer
2025-07-14 14:41:26 +02:00
committed by GitHub
parent 46dba7218c
commit 8de25f94a5
10 changed files with 84 additions and 46 deletions

View File

@@ -26,7 +26,7 @@ export class DOMLineBreaksComputerFactory implements ILineBreaksComputerFactory
constructor(private targetWindow: WeakRef<Window>) {
}
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 {

View File

@@ -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()),

View File

@@ -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 {

View File

@@ -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
}
/**

View File

@@ -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`:

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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);
}

31
src/vs/monaco.d.ts vendored
View File

@@ -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<EditorOption.wordWrapColumn, number>;
wordWrapOverride1: IEditorOption<EditorOption.wordWrapOverride1, 'on' | 'off' | 'inherit'>;
wordWrapOverride2: IEditorOption<EditorOption.wordWrapOverride2, 'on' | 'off' | 'inherit'>;
wrapOnEscapedLineFeeds: IEditorOption<EditorOption.wrapOnEscapedLineFeeds, boolean>;
effectiveCursorStyle: IEditorOption<EditorOption.effectiveCursorStyle, TextEditorCursorStyle>;
editorClassName: IEditorOption<EditorOption.editorClassName, string>;
defaultColorDecorators: IEditorOption<EditorOption.defaultColorDecorators, 'auto' | 'always' | 'never'>;