Skip to content

feat: support CR (\r) line endings in addition to LF and CRLF#302617

Open
jaykae wants to merge 3 commits intomicrosoft:mainfrom
jaykae:feature/cr-line-endings
Open

feat: support CR (\r) line endings in addition to LF and CRLF#302617
jaykae wants to merge 3 commits intomicrosoft:mainfrom
jaykae:feature/cr-line-endings

Conversation

@jaykae
Copy link

@jaykae jaykae commented Mar 18, 2026

Add CR (carriage return, \r) as a first-class end-of-line sequence, addressing #35797. This enables proper handling of files using CR-only line endings, such as HL7v2 healthcare files, pre-OSX Mac files, and various industrial protocol formats.

FULL DISCLOSURE - Completely AI Generated used Copilot and Opus but human reviewed and tests passing before PR submission

Changes across all layers:

Core enums:

  • Add CR to EndOfLineSequence, EndOfLinePreference, DefaultEndOfLine
  • Rename StringEOL.Invalid to StringEOL.CR (same value, 3)
  • Update standaloneEnums.ts and monaco.d.ts

Text buffer:

  • Widen type signatures to accept '\r' alongside '\r\n' and '\n'
  • Add CR case to _getEndOfLine() switch
  • Update EOL detection to recognize CR-only files
  • Update normalization condition for CR

Text model & services:

  • Update setEOL/getEndOfLineSequence/pushEOL for 3-way logic
  • Update modelService config mapping for CR

Configuration:

  • Add '\r' (CR) to files.eol setting enum

Status bar & UI:

  • Add 'CR' label to status bar indicator
  • Add CR option to Change End of Line Sequence quick-pick

Extension API:

  • Add EndOfLine.CR = 3 to public API (vscode.d.ts)
  • Update type converters and extHostDocumentData

Tests:

  • Add eolCounter.test.ts for CR detection
  • Add CR tests to pieceTreeTextBuffer.test.ts
  • Add CR tests to textModel.test.ts

Walkthrough: Adding CR (\r) Line Ending Support to VS Code (Updated 3/26/17 10PM EST)

As someone not familiar with the vscode codebase, I asked Copilot to generate some documentation on the changes

Issue: microsoft/vscode#35797
Branch: feature/cr-line-endings

Background

VS Code has always supported two types of line endings:

  • LF (\n) — Used by Linux, macOS (10.0+), and most modern Unix systems
  • CRLF (\r\n) — Used by Windows

However, a third line ending exists: CR (\r) — Used by pre-OSX classic Mac OS, and critically, still in active use today by:

  • HL7v2 — The international healthcare messaging standard, used by virtually every hospital and health system worldwide
  • Industrial protocols — Various electronics and controller systems
  • Retro computing — Classic Mac software preservation, Squeak/Pharo Smalltalk change files

When VS Code opens a file with CR-only line endings, it silently normalizes them to CRLF, corrupting the file. The files.eol setting rejects "\r" with:

Value is not accepted. Valid values: "\n", "\r\n", "auto"

This walkthrough explains every change needed to add CR as a first-class citizen throughout the VS Code editor stack.


Architecture Overview

Before diving into the code, it helps to understand how VS Code's EOL handling is layered:

┌─────────────────────────────────────────────────────────┐
│  User-facing layer                                      │
│  ┌──────────────┐  ┌─────────────────┐  ┌───────────┐   │
│  │ files.eol    │  │ Status Bar      │  │ Extension │   │
│  │ setting      │  │ "LF"/"CRLF"     │  │ API       │   │
│  └──────┬───────┘  └───────┬─────────┘  └─────┬─────┘   │
├─────────┼──────────────────┼──────────────────┼─────────┤
│  Model layer               │                  │         │
│  ┌──────┴───────┐  ┌───────┴─────────┐  ┌─────┴──────┐  │
│  │ ModelService │  │ TextModel       │  │ Type       │  │
│  │ (config→EOL) │  │ (setEOL/getEOL) │  │ Converters │  │
│  └──────┬───────┘  └───────┬─────────┘  └────────────┘  │
├─────────┼──────────────────┼────────────────────────────┤
│  Buffer layer              │                            │
│  ┌──────┴──────────────────┴─────────┐                  │
│  │ PieceTreeTextBuffer               │                  │
│  │ ┌─────────────────────────────┐   │                  │
│  │ │ PieceTreeBase               │   │                  │
│  │ │ (stores _EOL, normalizes)   │   │                  │
│  │ └─────────────────────────────┘   │                  │
│  └──────┬────────────────────────────┘                  │
│  ┌──────┴────────────────────────────┐                  │
│  │ PieceTreeTextBufferBuilder        │                  │
│  │ (detects EOL from file content)   │                  │
│  └──────┬────────────────────────────┘                  │
│  ┌──────┴────────────────────────────┐                  │
│  │ eolCounter.ts                     │                  │
│  │ (counts \r, \n, \r\n in text)     │                  │
│  └───────────────────────────────────┘                  │
├─────────────────────────────────────────────────────────┤
│  Enum definitions (model.ts)                            │
│       EndOfLineSequence,                                │
│       EndOfLinePreference,                              │
│       DefaultEndOfLine                                  │
└─────────────────────────────────────────────────────────┘

Data flows bottom-up when opening a file (detect → buffer → model → UI) and top-down when the user changes settings or picks a new EOL (UI → model → buffer → normalize).


Layer 1: Core Enum Definitions

Every component in VS Code references EOL through a set of TypeScript enums. These are the foundation — everything else depends on them.

1a. The Three Internal Enums

File: src/vs/editor/common/model.ts

VS Code uses three separate enums for different purposes:

// Used when READING text — "give me the text with this EOL style"
export const enum EndOfLinePreference {
    TextDefined = 0,  // Use whatever the buffer has
    LF = 1,
    CRLF = 2,
    CR = 3            // NEW
}

// Used when CREATING a new empty file — "what EOL should new files get?"
export const enum DefaultEndOfLine {
    LF = 1,
    CRLF = 2,
    CR = 3            // NEW
}

// Used when IDENTIFYING the EOL of an existing buffer — "what EOL does this file have?"
export const enum EndOfLineSequence {
    LF = 0,
    CRLF = 1,
    CR = 2            // NEW
}

Why three enums? They serve different roles:

  • EndOfLinePreference includes TextDefined (use whatever the buffer has) — used by APIs like getValueInRange() where you might want the text in a specific format
  • DefaultEndOfLine starts at 1 (not 0) — used in configuration where 0 would be falsy
  • EndOfLineSequence starts at 0 — used as the canonical identifier for a file's actual line endings

1b. The EOL Detection Enum

File: src/vs/editor/common/core/misc/eolCounter.ts

This enum is used internally by the low-level scanner that counts line endings in raw text:

export const enum StringEOL {
    Unknown = 0,
    LF = 1,
    CRLF = 2,
    CR = 4        // Was: Invalid = 3 — renamed and given a distinct bit value
}

Key insight: The values are chosen as bit flags so they can be OR'd together. When scanning text, the function does eol |= StringEOL.CR every time it finds a lone \r. A file with both CR and LF endings would have eol = 4 | 1 = 5 (distinct bits set for each type). The old code already detected lone CR — it just called it "Invalid" with the value 3. We renamed it CR and changed its value to 4 to give it a distinct bit that cannot collide with any OR-combination of LF (1) and CRLF (2), which sum to 3 when both are present.

The countEOL function that uses this enum already handled CR correctly:

export function countEOL(text: string): [number, number, number, StringEOL] {
    // ... scanning loop ...
    if (chr === CharCode.CarriageReturn) {
        if (i + 1 < len && text.charCodeAt(i + 1) === CharCode.LineFeed) {
            eol |= StringEOL.CRLF;  // \r\n pair
            i++;                      // skip the \n
        } else {
            eol |= StringEOL.CR;     // lone \r (was: StringEOL.Invalid)
        }
    } else if (chr === CharCode.LineFeed) {
        eol |= StringEOL.LF;         // lone \n
    }
    // ...
}

1c. The Standalone/Monaco Enums

Files: src/vs/editor/common/standalone/standaloneEnums.ts, src/vs/monaco.d.ts

These are mirror copies of the same enums, published for the Monaco editor (the standalone version of VS Code's editor). They are auto-generated files that need the same CR values added. The changes are identical to model.ts — just add the CR member to each enum.


Layer 2: Text Buffer — Where Text Lives

The PieceTree is VS Code's core data structure for storing file content. It's a balanced tree of string "pieces" that supports efficient insertion, deletion, and line-based access. EOL handling is deeply embedded here.

2a. PieceTreeBase — The Core Storage

File: src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts

The base class stores the EOL string and its length. Every type signature that previously accepted '\r\n' | '\n' now accepts '\r\n' | '\n' | '\r':

protected _EOL!: '\r\n' | '\n' | '\r';       // Was: '\r\n' | '\n'
protected _EOLLength!: number;                 // 1 for \n and \r, 2 for \r\n
protected _EOLNormalized!: boolean;

constructor(chunks: StringBuffer[], eol: '\r\n' | '\n' | '\r', eolNormalized: boolean) { ... }
create(chunks: StringBuffer[], eol: '\r\n' | '\n' | '\r', eolNormalized: boolean) { ... }
normalizeEOL(eol: '\r\n' | '\n' | '\r') { ... }
public getEOL(): '\r\n' | '\n' | '\r' { ... }
public setEOL(newEOL: '\r\n' | '\n' | '\r'): void { ... }

Critical insight: The normalizeEOL method uses the regex /\r\n|\r|\n/g to replace all line endings. This regex already handles CR! The order matters — \r\n is matched first (before the standalone \r), preventing CRLF from being split into CR + LF. No regex changes were needed.

normalizeEOL(eol: '\r\n' | '\n' | '\r') {
    // ...
    const text = tempChunk.replace(/\r\n|\r|\n/g, eol);  // Already works for CR!
    // ...
}

2b. PieceTreeTextBuffer — The Wrapper

File: src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts

This wraps PieceTreeBase and adds higher-level operations. Three changes:

1. Type signatures widened to accept '\r':

constructor(chunks: StringBuffer[], BOM: string, eol: '\r\n' | '\n' | '\r', ...)
public getEOL(): '\r\n' | '\n' | '\r' { ... }
public setEOL(newEOL: '\r\n' | '\n' | '\r'): void { ... }

2. The _getEndOfLine switch — This converts the EndOfLinePreference enum to an actual string. We add the CR case:

private _getEndOfLine(eol: EndOfLinePreference): string {
    switch (eol) {
        case EndOfLinePreference.LF:
            return '\n';
        case EndOfLinePreference.CRLF:
            return '\r\n';
        case EndOfLinePreference.CR:        // NEW
            return '\r';                     // NEW
        case EndOfLinePreference.TextDefined:
            return this.getEOL();
        default:
            throw new Error('Unknown EOL preference');
    }
}

3. The edit validation mapping — When text is inserted into a buffer, VS Code checks if the inserted text's line endings match the buffer's EOL. If not, it normalizes them. The mapping from buffer EOL string to StringEOL enum was binary and needed a third case:

const bufferEOL = this.getEOL();
// Was: (bufferEOL === '\r\n' ? StringEOL.CRLF : StringEOL.LF)
const expectedStrEOL = (bufferEOL === '\r\n' ? StringEOL.CRLF
    : bufferEOL === '\r' ? StringEOL.CR       // NEW
    : StringEOL.LF);

2c. PieceTreeTextBufferBuilder — The Factory

File: src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts

This is where VS Code detects which EOL a file uses when it's first opened. The builder accumulates counts of \r, \n, and \r\n as it reads file chunks, then the factory's _getEOL method decides which EOL "wins".

Before (binary decision):

private _getEOL(defaultEOL: DefaultEndOfLine): '\r\n' | '\n' {
    const totalEOLCount = this._cr + this._lf + this._crlf;
    const totalCRCount = this._cr + this._crlf;
    if (totalEOLCount === 0) {
        return (defaultEOL === DefaultEndOfLine.LF ? '\n' : '\r\n');
    }
    if (totalCRCount > totalEOLCount / 2) {
        return '\r\n';  // Lone \r was lumped in with \r\n!
    }
    return '\n';
}

After (three-way decision):

private _getEOL(defaultEOL: DefaultEndOfLine): '\r\n' | '\n' | '\r' {
    const totalEOLCount = this._cr + this._lf + this._crlf;
    if (totalEOLCount === 0) {
        // Empty file — use configured default
        if (defaultEOL === DefaultEndOfLine.LF) return '\n';
        if (defaultEOL === DefaultEndOfLine.CR) return '\r';
        return '\r\n';
    }
    if (this._cr > 0 && this._crlf === 0 && this._lf === 0) {
        // File ONLY contains \r line endings — this is a CR file
        return '\r';
    }
    const totalCRCount = this._cr + this._crlf;
    if (totalCRCount > totalEOLCount / 2) {
        return '\r\n';
    }
    return '\n';
}

Why the conservative detection? CR-only files are detected only when the file has exclusively CR endings (no LF or CRLF mixed in). Mixed files with some CR are still handled by the existing CRLF/LF heuristic. This prevents false positives — a file with accidental lone CRs won't suddenly be treated as a CR file.

The create() method's normalization condition also needed updating to handle the case where the detected EOL is CR but the file contains other EOL types:

if (this._normalizeEOL &&
    ((eol === '\r\n' && (this._cr > 0 || this._lf > 0))
        || (eol === '\n' && (this._cr > 0 || this._crlf > 0))
        || (eol === '\r' && (this._lf > 0 || this._crlf > 0)))  // NEW
) {
    // Normalize all chunks to the chosen EOL
}

Layer 3: Text Model — The Editor's View of the Buffer

File: src/vs/editor/common/model/textModel.ts

The TextModel is the high-level representation of a document. It wraps the buffer and provides the API that the rest of VS Code uses. Three methods needed updating from binary to three-way logic.

3a. setEOL — Change a file's line endings

Converts from the enum to the actual string. Was a simple ternary, now needs three branches:

public setEOL(eol: model.EndOfLineSequence): void {
    // Was: const newEOL = (eol === model.EndOfLineSequence.CRLF ? '\r\n' : '\n');
    const newEOL = (eol === model.EndOfLineSequence.CRLF ? '\r\n'
        : eol === model.EndOfLineSequence.CR ? '\r'
        : '\n');
    if (this._buffer.getEOL() === newEOL) return;  // Nothing to do
    // ... fire events, normalize buffer, etc.
}

3b. getEndOfLineSequence — Query a file's line endings

Converts from the buffer's string back to the enum. Was a ternary, now an if-chain:

public getEndOfLineSequence(): model.EndOfLineSequence {
    const eol = this._buffer.getEOL();
    if (eol === '\n') return model.EndOfLineSequence.LF;
    if (eol === '\r') return model.EndOfLineSequence.CR;
    return model.EndOfLineSequence.CRLF;
}

3c. pushEOL — Change line endings with undo support

This method compares the current EOL to the requested one. The old code used an inline binary conversion; we now reuse the getEndOfLineSequence() method:

public pushEOL(eol: model.EndOfLineSequence): void {
    // Was: const currentEOL = (this.getEOL() === '\n' ? ... LF : ... CRLF);
    const currentEOL = this.getEndOfLineSequence();
    if (currentEOL === eol) return;
    // ... push to undo stack, etc.
}

Layer 4: Model Service — Configuration Bridge

File: src/vs/editor/common/services/modelService.ts

The ModelService reads the files.eol configuration setting and translates it into the internal enum. Three places needed updating:

4a. Reading configuration

let newDefaultEOL = DEFAULT_EOL;
const eol = config.eol;
if (eol === '\r\n') {
    newDefaultEOL = DefaultEndOfLine.CRLF;
} else if (eol === '\n') {
    newDefaultEOL = DefaultEndOfLine.LF;
} else if (eol === '\r') {             // NEW
    newDefaultEOL = DefaultEndOfLine.CR; // NEW
}

4b. Applying configuration to models

When the setting changes and a model has only one line (no existing EOL to preserve), VS Code applies the new default:

model.setEOL(
    newOptions.defaultEOL === DefaultEndOfLine.LF ? EndOfLineSequence.LF
    : newOptions.defaultEOL === DefaultEndOfLine.CR ? EndOfLineSequence.CR  // NEW
    : EndOfLineSequence.CRLF
);

4c. Reconciling file content with model

When a file is reloaded from disk, the model service syncs the EOL:

model.pushEOL(
    textBuffer.getEOL() === '\r\n' ? EndOfLineSequence.CRLF
    : textBuffer.getEOL() === '\r' ? EndOfLineSequence.CR  // NEW
    : EndOfLineSequence.LF
);

Layer 5: The files.eol Setting

File: src/vs/workbench/contrib/files/browser/files.contribution.ts

This is where the actual user-facing setting is defined. The change is simple — add '\r' to the enum and its description:

'files.eol': {
    'type': 'string',
    'enum': [
        '\n',
        '\r\n',
        '\r',      // NEW
        'auto'
    ],
    'enumDescriptions': [
        nls.localize('eol.LF', "LF"),
        nls.localize('eol.CRLF', "CRLF"),
        nls.localize('eol.CR', "CR"),                                           // NEW
        nls.localize('eol.auto', "Uses operating system specific end of line character.")
    ],
    'default': 'auto',
    'description': nls.localize('eol', "The default end of line character."),
    'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE
}

Important: The textResourcePropertiesService.getEOL() method in src/vs/workbench/services/textresourceProperties/common/textResourcePropertiesService.ts already passes through the raw setting value for any non-'auto' string. No change was needed there — once the schema accepts '\r', the service will pass it through to the model layer automatically.

Users can now write this in their settings.json:

{
    "[hl7]": {
        "files.eol": "\r"
    }
}

Layer 6: Status Bar & UI

File: src/vs/workbench/browser/parts/editor/editorStatus.ts

The bottom status bar shows the current file's line endings ("LF" or "CRLF") and lets users click to change them.

6a. Add the localized label

const nlsEOLLF = localize('endOfLineLineFeed', "LF");
const nlsEOLCRLF = localize('endOfLineCarriageReturnLineFeed', "CRLF");
const nlsEOLCR = localize('endOfLineCarriageReturn', "CR");  // NEW

6b. Update the status bar display

The display logic was a binary ternary. Now it's a three-way chain:

// Was: this.state.EOL === '\r\n' ? nlsEOLCRLF : nlsEOLLF
this.state.EOL === '\r\n' ? nlsEOLCRLF
    : this.state.EOL === '\r' ? nlsEOLCR   // NEW
    : nlsEOLLF

6c. Update the "Change End of Line Sequence" quick-pick

When users click the status bar item, they get a picker. We add CR as an option:

const EOLOptions: IChangeEOLEntry[] = [
    { label: nlsEOLLF, eol: EndOfLineSequence.LF },
    { label: nlsEOLCRLF, eol: EndOfLineSequence.CRLF },
    { label: nlsEOLCR, eol: EndOfLineSequence.CR },   // NEW
];

// The active item selection also needs to handle 3 options:
const currentEOL = textModel?.getEOL();
const selectedIndex = currentEOL === '\r\n' ? 1 : currentEOL === '\r' ? 2 : 0;

Layer 7: Extension API

Extensions interact with VS Code through a typed API defined in vscode.d.ts. Three files needed changes to expose CR to extensions.

7a. Public API type definition

File: src/vscode-dts/vscode.d.ts

export enum EndOfLine {
    /** The line feed `\n` character. */
    LF = 1,
    /** The carriage return line feed `\r\n` sequence. */
    CRLF = 2,
    /** The carriage return `\r` character. */
    CR = 3    // NEW
}

7b. Internal type implementation

File: src/vs/workbench/api/common/extHostTypes/textEdit.ts

export enum EndOfLine {
    LF = 1,
    CRLF = 2,
    CR = 3     // NEW
}

7c. Type converters (internal ↔ extension API)

File: src/vs/workbench/api/common/extHostTypeConverters.ts

These functions translate between the internal EndOfLineSequence and the public EndOfLine:

export namespace EndOfLine {
    export function from(eol: vscode.EndOfLine): EndOfLineSequence | undefined {
        if (eol === types.EndOfLine.CRLF) return EndOfLineSequence.CRLF;
        if (eol === types.EndOfLine.LF) return EndOfLineSequence.LF;
        if (eol === types.EndOfLine.CR) return EndOfLineSequence.CR;   // NEW
        return undefined;
    }

    export function to(eol: EndOfLineSequence): vscode.EndOfLine | undefined {
        if (eol === EndOfLineSequence.CRLF) return types.EndOfLine.CRLF;
        if (eol === EndOfLineSequence.LF) return types.EndOfLine.LF;
        if (eol === EndOfLineSequence.CR) return types.EndOfLine.CR;   // NEW
        return undefined;
    }
}

7d. Extension host document data

File: src/vs/workbench/api/common/extHostDocumentData.ts

When an extension accesses document.eol, this getter maps the raw EOL string to the public enum:

// Was: that._eol === '\n' ? EndOfLine.LF : EndOfLine.CRLF
get eol() {
    return that._eol === '\n' ? EndOfLine.LF
        : that._eol === '\r' ? EndOfLine.CR   // NEW
        : EndOfLine.CRLF;
}

Layer 8: String Edit Utilities

File: src/vs/editor/common/core/edits/stringEdit.ts

The StringEdit and StringReplacement classes have normalizeEOL methods used during formatting operations. Their type signatures needed widening, and the regex in StringReplacement needed to include \r in its match pattern:

// StringEdit
public normalizeEOL(eol: '\r\n' | '\n' | '\r'): StringEdit {     // Was: '\r\n' | '\n'
    return new StringEdit(this.replacements.map(edit => edit.normalizeEOL(eol)));
}

// StringReplacement
normalizeEOL(eol: '\r\n' | '\n' | '\r'): StringReplacement {     // Was: '\r\n' | '\n'
    const newText = this.newText.replace(/\r\n|\r|\n/g, eol);     // Was: /\r\n|\n/g
    return new StringReplacement(this.replaceRange, newText);
}

The regex change from /\r\n|\n/g to /\r\n|\r|\n/g is important — the old regex would have left lone \r characters untouched when normalizing to LF or CRLF.


Layer 9: Tests

Three test files were added or modified, covering ~170 lines of new test code.

9a. eolCounter.test.ts (NEW FILE)

File: src/vs/editor/test/common/core/misc/eolCounter.test.ts

Tests the low-level countEOL function's ability to detect CR line endings:

test('CR line endings', () => {
    const [eolCount, firstLineLength, lastLineLength, eol] = countEOL('line1\rline2\rline3');
    assert.strictEqual(eolCount, 2);
    assert.strictEqual(firstLineLength, 5);
    assert.strictEqual(lastLineLength, 5);
    assert.strictEqual(eol, StringEOL.CR);
});

test('mixed CR and LF', () => {
    const [eolCount, , , eol] = countEOL('line1\rline2\nline3');
    assert.strictEqual(eolCount, 2);
    // Bit flags: CR (4) | LF (1) = 5 — distinct bits, unambiguous
    assert.strictEqual(eol, StringEOL.CR | StringEOL.LF);
    assert.strictEqual(eol & StringEOL.CR, StringEOL.CR);
    assert.strictEqual(eol & StringEOL.LF, StringEOL.LF);
    assert.strictEqual(eol & StringEOL.CRLF, 0);  // CRLF bit is NOT set
});

9b. pieceTreeTextBuffer.test.ts

File: src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts

A new 'CR line endings' test suite with 5 tests:

test('CR-only file is detected and preserved', () => {
    const pieceTree = createTextBuffer(['line1\rline2\rline3'], true);
    assert.strictEqual(pieceTree.getEOL(), '\r');
    assert.strictEqual(pieceTree.getLineCount(), 3);
    assert.strictEqual(pieceTree.getLineContent(1), 'line1');
});

test('setEOL to CR normalizes line endings', () => {
    const pieceTree = createTextBuffer(['line1\nline2\nline3'], true);
    pieceTree.setEOL('\r');
    assert.strictEqual(pieceTree.getEOL(), '\r');
    assert.strictEqual(pieceTree.getLineCount(), 3);
});

test('CR detection prefers CR when file has only CR endings', () => {
    const bufferBuilder = new PieceTreeTextBufferBuilder();
    bufferBuilder.acceptChunk('a\rb\rc\r');
    const factory = bufferBuilder.finish(true);
    const { textBuffer } = factory.create(DefaultEndOfLine.LF);
    assert.strictEqual(textBuffer.getEOL(), '\r');   // CR detected despite LF default
    assert.strictEqual(textBuffer.getLineCount(), 4);
});

9c. textModel.test.ts

File: src/vs/editor/test/common/model/textModel.test.ts

Three new tests at the TextModel level:

test('getValueLengthInRange with CR EOL', () => {
    const m = createTextModel('My First Line\rMy Second Line\rMy Third Line');
    assert.strictEqual(m.getEOL(), '\r');
    assert.strictEqual(m.getEndOfLineSequence(), EndOfLineSequence.CR);
    // Verify CR preference returns correct length (1 char per EOL)
    assert.strictEqual(
        m.getValueLengthInRange(new Range(1, 1, 2, 1), EndOfLinePreference.CR),
        'My First Line\r'.length
    );
    // Verify CRLF preference returns correct length (2 chars per EOL)
    assert.strictEqual(
        m.getValueLengthInRange(new Range(1, 1, 2, 1), EndOfLinePreference.CRLF),
        'My First Line\r\n'.length
    );
});

test('setEOL and getEndOfLineSequence with CR', () => {
    const m = createTextModel('line1\nline2\nline3');
    m.setEOL(EndOfLineSequence.CR);
    assert.strictEqual(m.getEOL(), '\r');
    assert.strictEqual(m.getEndOfLineSequence(), EndOfLineSequence.CR);
});

test('pushEOL with CR', () => {
    const m = createTextModel('line1\nline2\nline3');
    m.pushEOL(EndOfLineSequence.CR);
    assert.strictEqual(m.getValue(), 'line1\rline2\rline3');  // Content normalized
});

What Didn't Need to Change (and Why)

Several files were reviewed but needed no modification:

File Reason
textResourcePropertiesService.ts Already passes through raw files.eol string for non-'auto' values
textModelSearch.ts The lfCounter is only needed for CRLF→LF offset compensation (CRLF is 2 chars, LF is 1). CR is also 1 char, so no compensation needed
textModel.ts line 1314 (trailing CR stripping) Only applies when buffer is CRLF and edit text ends with lone \r — CR mode shouldn't strip trailing \r since that's the actual line ending
pieceTreeBase.ts normalizeEOL regex The regex `/\r\n

Summary of All Changed Files

# File Lines Changed What
1 src/vs/editor/common/model.ts +12 Add CR to 3 enums + ITextBuffer interface
2 src/vs/editor/common/core/misc/eolCounter.ts +2 -2 Rename InvalidCR, change value from 3 to 4 (distinct bit)
3 src/vs/editor/common/standalone/standaloneEnums.ts +12 Mirror enum changes
4 src/vs/monaco.d.ts +12 Mirror enum changes
5 src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +6 -6 Widen type signatures
6 src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +5 -3 CR case in switch + type sigs
7 src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts +14 -5 CR detection logic
8 src/vs/editor/common/model/textModel.ts +11 -5 3-way EOL in setEOL/getEndOfLineSequence/pushEOL
9 src/vs/editor/common/services/modelService.ts +4 -2 CR config mapping
10 src/vs/editor/common/core/edits/stringEdit.ts +3 -3 Widen types + fix regex
11 src/vs/workbench/contrib/files/browser/files.contribution.ts +2 Add '\r' to setting
12 src/vs/workbench/browser/parts/editor/editorStatus.ts +5 -2 CR label + quick-pick
13 src/vs/workbench/api/common/extHostTypes/textEdit.ts +1 CR = 3
14 src/vs/workbench/api/common/extHostTypeConverters.ts +4 CR conversion
15 src/vs/workbench/api/common/extHostDocumentData.ts +1 -1 CR in eol getter
16 src/vscode-dts/vscode.d.ts +4 Public API CR = 3
17 src/vs/editor/test/common/core/misc/eolCounter.test.ts +70 NEW: CR detection tests
18 src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts +54 CR buffer tests
19 src/vs/editor/test/common/model/textModel.test.ts +48 CR model tests

Total: 19 files, +281 insertions, -45 deletions

Copilot AI review requested due to automatic review settings March 18, 2026 00:43
@vs-code-engineering
Copy link
Contributor

vs-code-engineering bot commented Mar 18, 2026

📬 CODENOTIFY

The following users are being notified based on files changed in this PR:

@bpasero

Matched files:

  • src/vs/workbench/browser/parts/editor/editorStatus.ts
  • src/vs/workbench/contrib/files/browser/files.contribution.ts

@jaykae
Copy link
Author

jaykae commented Mar 18, 2026

@microsoft-github-policy-service agree company="Alara Imaging"

jaykae and others added 2 commits March 17, 2026 20:46
Add CR (carriage return, \r) as a first-class end-of-line sequence,
addressing microsoft#35797. This enables proper handling of
files using CR-only line endings, such as HL7v2 healthcare files,
pre-OSX Mac files, and various industrial protocol formats.

Changes across all layers:

Core enums:
- Add CR to EndOfLineSequence, EndOfLinePreference, DefaultEndOfLine
- Rename StringEOL.Invalid to StringEOL.CR (same value, 3)
- Update standaloneEnums.ts and monaco.d.ts

Text buffer:
- Widen type signatures to accept '\r' alongside '\r\n' and '\n'
- Add CR case to _getEndOfLine() switch
- Update EOL detection to recognize CR-only files
- Update normalization condition for CR

Text model & services:
- Update setEOL/getEndOfLineSequence/pushEOL for 3-way logic
- Update modelService config mapping for CR

Configuration:
- Add '\r' (CR) to files.eol setting enum

Status bar & UI:
- Add 'CR' label to status bar indicator
- Add CR option to Change End of Line Sequence quick-pick

Extension API:
- Add EndOfLine.CR = 3 to public API (vscode.d.ts)
- Update type converters and extHostDocumentData

Tests:
- Add eolCounter.test.ts for CR detection
- Add CR tests to pieceTreeTextBuffer.test.ts
- Add CR tests to textModel.test.ts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class support for CR-only (\r) line endings across the editor stack to avoid corrupting CR-terminated files on open/edit/save, addressing #35797.

Changes:

  • Extend core EOL enums/types and propagate '\r' support through text buffer + text model logic.
  • Surface CR in configuration (files.eol) and UI (status bar indicator + “Change End of Line Sequence” picker).
  • Expose CR through the extension API and add/extend tests for CR detection and behavior.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/vs/editor/common/model.ts Adds CR to core EOL enums and widens ITextBuffer.setEOL to accept '\r'.
src/vs/editor/common/core/misc/eolCounter.ts Renames “Invalid” to CR in low-level EOL detection.
src/vs/editor/common/standalone/standaloneEnums.ts Mirrors new CR enum values for standalone editor consumers.
src/vs/monaco.d.ts Mirrors new CR enum values for Monaco typings.
src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts Widens internal EOL types to include '\r'.
src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts Adds CR preference handling and supports CR buffer EOL.
src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts Detects CR-only files and supports CR defaults.
src/vs/editor/common/model/textModel.ts Updates model EOL conversions for LF/CRLF/CR.
src/vs/editor/common/services/modelService.ts Maps files.eol to CR and keeps models in sync with CR.
src/vs/editor/common/core/edits/stringEdit.ts Normalizes \r during edit normalization and widens types.
src/vs/workbench/contrib/files/browser/files.contribution.ts Adds '\r' to the files.eol setting enum + label.
src/vs/workbench/browser/parts/editor/editorStatus.ts Adds CR label and quick-pick option for changing EOL.
src/vs/workbench/api/common/extHostTypes/textEdit.ts Adds EndOfLine.CR = 3 to ext host types.
src/vs/workbench/api/common/extHostTypeConverters.ts Converts CR between extension API and internal model enums.
src/vs/workbench/api/common/extHostDocumentData.ts Exposes CR via document.eol mapping.
src/vscode-dts/vscode.d.ts Adds EndOfLine.CR = 3 to the public extension API typings.
src/vs/editor/test/common/core/misc/eolCounter.test.ts New test coverage for CR detection and mixed EOL cases.
src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts Adds CR buffer behavior tests (detection, setEOL, round-trip).
src/vs/editor/test/common/model/textModel.test.ts Adds CR-focused model tests for value length and EOL changes.
Comments suppressed due to low confidence (1)

src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts:288

  • With the current StringEOL values, expectedStrEOL can match mixed-EOL insert text when the buffer is in CR mode. For example, an inserted string containing both \n and \r\n will produce strEOL === 3, which equals StringEOL.CR in this PR, so the code will skip normalization and allow mixed EOLs into a CR buffer. Fixing StringEOL to use non-overlapping bit values (or adjusting the comparison to properly detect single-EOL text) will prevent this.
				const bufferEOL = this.getEOL();
				const expectedStrEOL = (bufferEOL === '\r\n' ? StringEOL.CRLF : bufferEOL === '\r' ? StringEOL.CR : StringEOL.LF);
				if (strEOL === StringEOL.Unknown || strEOL === expectedStrEOL) {
					validText = op.text;
				} else {
					validText = op.text.replace(/\r\n|\r|\n/g, bufferEOL);
				}

You can also share your feedback on Copilot code review. Take the survey.

- Change StringEOL.CR from 3 to 4 to avoid collision with LF|CRLF (1|2=3).
  With CR=3, mixed LF+CRLF text was indistinguishable from CR-only text,
  causing edit normalization to be silently skipped for CR buffers.
- Restore missing test('guess indentation 1') wrapper that was accidentally
  removed when CR tests were inserted, leaving assertGuess() calls orphaned.
- Update eolCounter mixed-EOL test assertions for the new bitmask values
  and add a mixed LF+CRLF test to verify CR=4 is distinct from LF|CRLF=3.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class support for CR (\r) line endings across VS Code’s editor stack (buffer → model → workbench UI → extension API), addressing #35797 and preventing unwanted normalization/corruption of CR-only files.

Changes:

  • Extend core EOL enums and EOL detection to represent CR alongside LF/CRLF.
  • Propagate CR support through PieceTree text buffers, TextModel, model service/config mapping, and UI (status bar + quick pick).
  • Expose CR in the public extension API and add/extend tests for detection and round-tripping.

Reviewed changes

Copilot reviewed 17 out of 19 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/vs/editor/common/model.ts Adds CR to internal EOL enums and widens ITextBuffer EOL typing.
src/vs/editor/common/core/misc/eolCounter.ts Renames/repurposes the “invalid” EOL flag to CR with a distinct bit value.
src/vs/editor/common/standalone/standaloneEnums.ts Mirrors updated EOL enums for the standalone Monaco surface (generated output).
src/vs/monaco.d.ts Mirrors updated EOL enums for Monaco’s declaration surface.
src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts Allows CR as a stored/normalized EOL in the core piece tree buffer.
src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts Supports CR for EOL preference resolution and edit EOL normalization.
src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts Detects CR-only files and updates normalization conditions.
src/vs/editor/common/model/textModel.ts Updates set/get/push EOL logic to handle CR as a third option.
src/vs/editor/common/services/modelService.ts Maps files.eol = \r into DefaultEndOfLine and reconciles EOL on reload.
src/vs/editor/common/core/edits/stringEdit.ts Extends EOL normalization utilities to include CR.
src/vs/workbench/contrib/files/browser/files.contribution.ts Allows \r as a valid files.eol configuration value.
src/vs/workbench/browser/parts/editor/editorStatus.ts Displays “CR” in the status bar and adds a CR pick option.
src/vs/workbench/api/common/extHostTypes/textEdit.ts Adds EndOfLine.CR to the extension host type.
src/vs/workbench/api/common/extHostTypeConverters.ts Converts CR between API and internal model enums.
src/vs/workbench/api/common/extHostDocumentData.ts Exposes CR via TextDocument.eol to extensions.
src/vscode-dts/vscode.d.ts Exposes CR in the public vscode.EndOfLine API.
src/vs/editor/test/common/core/misc/eolCounter.test.ts Adds tests for CR detection and mixed-EOL bit flag behavior.
src/vs/editor/test/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.test.ts Adds buffer-level CR detection/normalization and round-trip tests.
src/vs/editor/test/common/model/textModel.test.ts Adds TextModel-level tests for CR preference, setEOL, and pushEOL behavior.

You can also share your feedback on Copilot code review. Take the survey.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants