Snippet

Calculate editor line, column, and newline style safely

A complete Swift utility for UTF-16 editor offsets, Unicode-aware character counts, newline detection, and large-file warning policy.

swiftOct 6, 2026MIT

AI-generated, human-reviewed: the first draft was generated by an AI agent; I fact-checked, tested, and edited it. AI use policy

What it does#

Takes an AppKit selection offset, which is UTF-16-based, and returns a human-readable line, column, and character count. It also detects LF, CRLF, CR, mixed, or no newlines. This is the part where a waving-hand emoji reminds you that strings are not arrays. Rude, but accurate.

You need#

  • macOS 15 or later
  • Xcode 27 or Swift 6.4
  • Foundation only
  • A text editor that reports selection locations as UTF-16 offsets, such as NSTextView

Where it goes#

Save it as EditorTextMetrics.swift in your core text module. Use it from your status bar.

EditorTextMetrics.swift
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Sarang Datye
import Foundation

let editorLargeFileWarningThresholdBytes = 10 * 1024 * 1024

enum NewlineStyle: String, Codable, Equatable, Sendable {
    case lf = "LF"
    case crlf = "CRLF"
    case cr = "CR"
    case mixed = "Mixed"
    case none = "None"
}

enum NewlineDetector {
    static func detect(in text: String) -> NewlineStyle {
        var lf = 0
        var crlf = 0
        var cr = 0
        var index = text.unicodeScalars.startIndex

        while index < text.unicodeScalars.endIndex {
            let scalar = text.unicodeScalars[index]
            if scalar == "\r" {
                let next = text.unicodeScalars.index(after: index)
                if next < text.unicodeScalars.endIndex && text.unicodeScalars[next] == "\n" {
                    crlf += 1
                    index = text.unicodeScalars.index(after: next)
                } else {
                    cr += 1
                    index = next
                }
            } else if scalar == "\n" {
                lf += 1
                index = text.unicodeScalars.index(after: index)
            } else {
                index = text.unicodeScalars.index(after: index)
            }
        }

        let nonZero = [lf, crlf, cr].filter { $0 > 0 }.count
        if nonZero == 0 { return .none }
        if nonZero > 1 { return .mixed }
        if crlf > 0 { return .crlf }
        if cr > 0 { return .cr }
        return .lf
    }

    static func newlineString(for style: NewlineStyle) -> String {
        switch style {
        case .lf, .mixed, .none: return "\n"
        case .crlf: return "\r\n"
        case .cr: return "\r"
        }
    }
}

struct EditorTextMetrics: Codable, Equatable, Sendable {
    var line: Int
    var column: Int
    var characterCount: Int
}

enum EditorTextMetricsCalculator {
    static func metrics(text: String, selectionLocation: Int) -> EditorTextMetrics {
        let boundedOffset = max(0, min(selectionLocation, text.utf16.count))
        var utf16Index = text.utf16.index(text.utf16.startIndex, offsetBy: boundedOffset)
        var prefixEnd = String.Index(utf16Index, within: text)

        while prefixEnd == nil && utf16Index > text.utf16.startIndex {
            utf16Index = text.utf16.index(before: utf16Index)
            prefixEnd = String.Index(utf16Index, within: text)
        }

        let prefix = text[..<(prefixEnd ?? text.startIndex)]
        var line = 1
        var column = 1

        // Count grapheme clusters, the same unit as characterCount; Swift treats CRLF as one Character.
        for character in prefix {
            if character == "\r\n" || character == "\n" || character == "\r" {
                line += 1
                column = 1
            } else {
                column += 1
            }
        }

        return EditorTextMetrics(line: line, column: column, characterCount: text.count)
    }
}

enum EditorFileSizePolicy {
    static func shouldWarn(byteCount: Int64) -> Bool {
        byteCount > Int64(editorLargeFileWarningThresholdBytes)
    }
}

Check that it works#

Call NewlineDetector.detect(in: "a\r\nb") and expect .crlf. Then call EditorTextMetricsCalculator.metrics(text: "hi\n\u{1F44B} world", selectionLocation: 5) and expect line 2, column 2, and a character count equal to text.count.