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.
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.
// 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.