docs(release): expand v0.4.2-beta notes for new syntax and detection coverage

Update README and CHANGELOG for the existing v0.4.2-beta release without bumping
the version, documenting the latest editor language support improvements.

- Add v0.4.2-beta notes for syntax highlighting support in vim, log, and ipynb files
- Document new extension/dotfile detection for .vim, .log, .ipynb, and .vimrc
- Clarify .h default mapping to cpp for practical C/C++ header highlighting
- Record language picker/menu additions for Vim, Log, and Jupyter Notebook
- Keep release tag/version unchanged (still v0.4.2-beta)
This commit is contained in:
h3p 2026-02-08 12:14:49 +01:00
parent 8f1e181187
commit 4293981992
11 changed files with 459 additions and 61 deletions

View file

@ -6,6 +6,14 @@ The format follows *Keep a Changelog*. Versions use semantic versioning with pre
## [v0.4.2-beta] - 2026-02-08
### Added
- Syntax highlighting profiles for **Vim** (`.vim`), **Log** (`.log`), and **Jupyter Notebook JSON** (`.ipynb`).
- Language picker/menu entries for `vim`, `log`, and `ipynb` across toolbar and app command menus.
### Improved
- Extension and dotfile language detection for `.vim`, `.log`, `.ipynb`, and `.vimrc`.
- Header-file default mapping by treating `.h` as `cpp` for more practical C/C++ highlighting.
### Fixed
- Scoped toolbar and menu commands to the active window to avoid cross-window side effects.
- Routed command actions to the focused window's `EditorViewModel` in multi-window workflows.

View file

@ -264,7 +264,7 @@
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 102;
CURRENT_PROJECT_VERSION = 104;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;
@ -336,7 +336,7 @@
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 102;
CURRENT_PROJECT_VERSION = 104;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;

View file

@ -42,7 +42,7 @@ extension ContentView {
@ViewBuilder
private var languagePickerControl: some View {
Picker("Language", selection: currentLanguageBinding) {
ForEach(["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in
ForEach(["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "vim", "log", "ipynb", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in
let label: String = {
switch lang {
case "php": return "PHP"
@ -56,6 +56,9 @@ extension ContentView {
case "csv": return "CSV"
case "ini": return "INI"
case "sql": return "SQL"
case "vim": return "Vim"
case "log": return "Log"
case "ipynb": return "Jupyter Notebook"
case "html": return "HTML"
case "css": return "CSS"
case "standard": return "Standard"
@ -288,7 +291,7 @@ extension ContentView {
#else
ToolbarItemGroup(placement: .automatic) {
Picker("Language", selection: currentLanguageBinding) {
ForEach(["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in
ForEach(["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "vim", "log", "ipynb", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in
let label: String = {
switch lang {
case "php": return "PHP"
@ -302,6 +305,9 @@ extension ContentView {
case "csv": return "CSV"
case "ini": return "INI"
case "sql": return "SQL"
case "vim": return "Vim"
case "log": return "Log"
case "ipynb": return "Jupyter Notebook"
case "html": return "HTML"
case "css": return "CSS"
case "standard": return "Standard"

View file

@ -86,6 +86,11 @@ struct ContentView: View {
@State var quickSwitcherQuery: String = ""
@State var vimModeEnabled: Bool = UserDefaults.standard.bool(forKey: "EditorVimModeEnabled")
@State var vimInsertMode: Bool = true
@State var droppedFileLoadInProgress: Bool = false
@State var droppedFileProgressDeterminate: Bool = true
@State var droppedFileLoadProgress: Double = 0
@State var droppedFileLoadLabel: String = ""
@State var largeFileModeEnabled: Bool = false
@AppStorage("HasSeenWelcomeTourV1") var hasSeenWelcomeTourV1: Bool = false
@State var showWelcomeTour: Bool = false
#if os(macOS)
@ -724,12 +729,57 @@ struct ContentView: View {
caretStatus = "Ln \(line), Col \(col)"
}
}
.onReceive(NotificationCenter.default.publisher(for: .pastedText)) { notif in
if let pasted = notif.object as? String {
let result = LanguageDetector.shared.detect(text: pasted, name: nil, fileURL: nil)
currentLanguageBinding.wrappedValue = result.lang == "plain" ? "swift" : result.lang
}
.onReceive(NotificationCenter.default.publisher(for: .pastedText)) { notif in
if let pasted = notif.object as? String {
let result = LanguageDetector.shared.detect(text: pasted, name: nil, fileURL: nil)
currentLanguageBinding.wrappedValue = result.lang == "plain" ? "swift" : result.lang
}
}
.onReceive(NotificationCenter.default.publisher(for: .droppedFileURL)) { notif in
guard let fileURL = notif.object as? URL else { return }
if let preferred = LanguageDetector.shared.preferredLanguage(for: fileURL) {
currentLanguageBinding.wrappedValue = preferred
}
}
.onReceive(NotificationCenter.default.publisher(for: .droppedFileLoadStarted)) { notif in
droppedFileLoadInProgress = true
droppedFileProgressDeterminate = (notif.userInfo?["isDeterminate"] as? Bool) ?? true
droppedFileLoadProgress = 0
droppedFileLoadLabel = "Reading file"
largeFileModeEnabled = (notif.userInfo?["largeFileMode"] as? Bool) ?? false
}
.onReceive(NotificationCenter.default.publisher(for: .droppedFileLoadProgress)) { notif in
// Recover even if "started" was missed.
droppedFileLoadInProgress = true
let fraction: Double = {
if let v = notif.userInfo?["fraction"] as? Double { return v }
if let v = notif.userInfo?["fraction"] as? NSNumber { return v.doubleValue }
if let v = notif.userInfo?["fraction"] as? Float { return Double(v) }
if let v = notif.userInfo?["fraction"] as? CGFloat { return Double(v) }
return droppedFileLoadProgress
}()
droppedFileLoadProgress = min(max(fraction, 0), 1)
if (notif.userInfo?["largeFileMode"] as? Bool) == true {
largeFileModeEnabled = true
}
if let name = notif.userInfo?["fileName"] as? String, !name.isEmpty {
droppedFileLoadLabel = name
}
}
.onReceive(NotificationCenter.default.publisher(for: .droppedFileLoadFinished)) { notif in
let success = (notif.userInfo?["success"] as? Bool) ?? true
droppedFileLoadProgress = success ? 1 : 0
if (notif.userInfo?["largeFileMode"] as? Bool) == true {
largeFileModeEnabled = true
}
if !success, let message = notif.userInfo?["message"] as? String, !message.isEmpty {
findStatusMessage = "Drop failed: \(message)"
droppedFileLoadLabel = "Import failed"
}
DispatchQueue.main.asyncAfter(deadline: .now() + (success ? 0.35 : 2.5)) {
droppedFileLoadInProgress = false
}
}
}
private func withCommandEvents<Content: View>(_ view: Content) -> some View {
@ -1040,7 +1090,7 @@ struct ContentView: View {
/// Returns a supported language string used by syntax highlighting and the language picker.
private func detectLanguageWithAppleIntelligence(_ text: String) async -> String {
// Supported languages in our picker
let supported = ["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "objective-c", "csharp", "json", "xml", "yaml", "toml", "csv", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"]
let supported = ["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "objective-c", "csharp", "json", "xml", "yaml", "toml", "csv", "ini", "vim", "log", "ipynb", "markdown", "bash", "zsh", "powershell", "standard", "plain"]
#if USE_FOUNDATION_MODELS
// Attempt a lightweight model-based detection via AppleIntelligenceAIClient if available
@ -1182,6 +1232,7 @@ struct ContentView: View {
colorScheme: colorScheme,
fontSize: editorFontSize,
isLineWrapEnabled: $viewModel.isLineWrapEnabled,
isLargeFileMode: largeFileModeEnabled,
translucentBackgroundEnabled: enableTranslucentWindow
)
.id(currentLanguage)
@ -1234,6 +1285,28 @@ struct ContentView: View {
.toolbar {
editorToolbarContent
}
.overlay(alignment: .topTrailing) {
if droppedFileLoadInProgress {
HStack(spacing: 8) {
if droppedFileProgressDeterminate {
ProgressView(value: droppedFileLoadProgress)
.progressViewStyle(.linear)
.frame(width: 120)
} else {
ProgressView()
.frame(width: 16)
}
Text(droppedFileProgressDeterminate ? "\(droppedFileLoadLabel) \(importProgressPercentText)" : "\(droppedFileLoadLabel) Loading…")
.font(.system(size: 11, weight: .medium))
.lineLimit(1)
}
.padding(.horizontal, 10)
.padding(.vertical, 7)
.background(.ultraThinMaterial, in: Capsule(style: .continuous))
.padding(.top, viewModel.isBrainDumpMode ? 12 : 50)
.padding(.trailing, 12)
}
}
#if os(macOS)
.toolbarBackground(enableTranslucentWindow ? AnyShapeStyle(.ultraThinMaterial) : AnyShapeStyle(Color(nsColor: .windowBackgroundColor)), for: ToolbarPlacement.windowToolbar)
#else
@ -1244,9 +1317,40 @@ struct ContentView: View {
// Status line: caret location + live word count from the view model.
@ViewBuilder
var wordCountView: some View {
HStack {
HStack(spacing: 10) {
if droppedFileLoadInProgress {
HStack(spacing: 8) {
if droppedFileProgressDeterminate {
ProgressView(value: droppedFileLoadProgress)
.progressViewStyle(.linear)
.frame(width: 130)
} else {
ProgressView()
.frame(width: 18)
}
Text(droppedFileProgressDeterminate ? "\(droppedFileLoadLabel) \(importProgressPercentText)" : "\(droppedFileLoadLabel) Loading…")
.font(.system(size: 11))
.foregroundColor(.secondary)
.lineLimit(1)
}
.padding(.leading, 12)
}
if largeFileModeEnabled {
Text("Large File Mode")
.font(.system(size: 11, weight: .semibold))
.foregroundColor(.secondary)
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(
Capsule(style: .continuous)
.fill(Color.secondary.opacity(0.16))
)
}
Spacer()
Text("\(caretStatus) • Words: \(viewModel.wordCount(for: currentContent))\(vimStatusSuffix)")
Text(largeFileModeEnabled
? "\(caretStatus)\(vimStatusSuffix)"
: "\(caretStatus) • Words: \(viewModel.wordCount(for: currentContent))\(vimStatusSuffix)")
.font(.system(size: 12))
.foregroundColor(.secondary)
.padding(.bottom, 8)
@ -1306,6 +1410,12 @@ struct ContentView: View {
#endif
}
private var importProgressPercentText: String {
let clamped = min(max(droppedFileLoadProgress, 0), 1)
if clamped > 0, clamped < 0.01 { return "1%" }
return "\(Int(clamped * 100))%"
}
private var quickSwitcherItems: [QuickFileSwitcherPanel.Item] {
var items: [QuickFileSwitcherPanel.Item] = []
let fileURLSet = Set(viewModel.tabs.compactMap { $0.fileURL?.standardizedFileURL.path })

View file

@ -14,6 +14,7 @@ final class AcceptingTextView: NSTextView {
private var isVimInsertMode: Bool = true
private var vimObservers: [NSObjectProtocol] = []
private var didConfigureVimMode: Bool = false
private let dropReadChunkSize = 64 * 1024
// We want the caret at the *start* of the paste.
private var pendingPasteCaretLocation: Int?
@ -65,49 +66,246 @@ final class AcceptingTextView: NSTextView {
let first = nsurls.first {
let url: URL = first as URL
let didAccess = url.startAccessingSecurityScopedResource()
defer { if didAccess { url.stopAccessingSecurityScopedResource() } }
do {
// Read file contents with security-scoped access
let content: String
if let data = try? Data(contentsOf: url) {
if let s = String(data: data, encoding: .utf8) {
content = s
} else if let s = String(data: data, encoding: .utf16) {
content = s
} else {
content = try String(contentsOf: url, encoding: .utf8)
let selectionAtDrop = clampedSelectionRange()
let fileName = url.lastPathComponent
let attributes = try? FileManager.default.attributesOfItem(atPath: url.path)
let attrSize = (attributes?[.size] as? NSNumber)?.int64Value ?? 0
let resourceSize = (try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize).map { Int64($0) } ?? 0
let totalBytes = max(attrSize, resourceSize)
let largeFileMode = totalBytes >= 1_000_000
NotificationCenter.default.post(
name: .droppedFileLoadStarted,
object: nil,
userInfo: [
"fileName": fileName,
"totalBytes": totalBytes,
"largeFileMode": largeFileMode,
"isDeterminate": totalBytes > 0
]
)
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
defer {
if didAccess {
url.stopAccessingSecurityScopedResource()
}
}
guard let self else { return }
do {
let data = try self.readDroppedFileData(at: url, totalBytes: totalBytes) { fraction in
NotificationCenter.default.post(
name: .droppedFileLoadProgress,
object: nil,
userInfo: [
"fraction": fraction * 0.55,
"fileName": "Reading file",
"largeFileMode": largeFileMode
]
)
}
let content = self.decodeDroppedFileText(data, fileURL: url)
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.applyDroppedContentInChunks(
content,
at: selectionAtDrop,
fileName: fileName,
largeFileMode: largeFileMode || data.count >= 1_000_000
) { success, insertedLength in
guard success else {
NotificationCenter.default.post(
name: .droppedFileLoadFinished,
object: nil,
userInfo: [
"success": false,
"fileName": fileName,
"largeFileMode": largeFileMode || data.count >= 1_000_000,
"message": "Failed while applying dropped content."
]
)
return
}
let newLoc = selectionAtDrop.location + insertedLength
self.setSelectedRange(NSRange(location: newLoc, length: 0))
self.scrollRangeToVisible(NSRange(location: selectionAtDrop.location, length: insertedLength))
self.didChangeText()
NotificationCenter.default.post(name: .droppedFileURL, object: url)
if insertedLength <= 120_000 {
NotificationCenter.default.post(name: .pastedText, object: content)
}
NotificationCenter.default.post(
name: .droppedFileLoadFinished,
object: nil,
userInfo: [
"success": true,
"fileName": fileName,
"largeFileMode": largeFileMode || data.count >= 1_000_000
]
)
}
}
} catch {
DispatchQueue.main.async {
NotificationCenter.default.post(
name: .droppedFileLoadFinished,
object: nil,
userInfo: [
"success": false,
"fileName": fileName,
"largeFileMode": largeFileMode,
"message": error.localizedDescription
]
)
}
} else {
content = try String(contentsOf: url, encoding: .utf8)
}
// Replace current selection with the dropped file contents
let nsContent = content as NSString
let sel = selectedRange()
undoManager?.disableUndoRegistration()
textStorage?.beginEditing()
textStorage?.mutableString.replaceCharacters(in: sel, with: nsContent as String)
textStorage?.endEditing()
undoManager?.enableUndoRegistration()
// Notify the text system so delegates/SwiftUI binding update
self.didChangeText()
// Move caret to the end of inserted content and reveal range
let newLoc = sel.location + nsContent.length
setSelectedRange(NSRange(location: newLoc, length: 0))
// Ensure the full inserted range is visible
let insertedRange = NSRange(location: sel.location, length: nsContent.length)
scrollRangeToVisible(insertedRange)
NotificationCenter.default.post(name: .pastedText, object: content)
return true
} catch {
return false
}
return true
}
return false
}
// MARK: - Typing helpers (your existing behavior)
private func applyDroppedContentInChunks(
_ content: String,
at selection: NSRange,
fileName: String,
largeFileMode: Bool,
completion: @escaping (Bool, Int) -> Void
) {
let nsContent = content as NSString
let safeSelection = clampedRange(selection, forTextLength: (string as NSString).length)
let total = nsContent.length
if total == 0 {
completion(true, 0)
return
}
NotificationCenter.default.post(
name: .droppedFileLoadProgress,
object: nil,
userInfo: [
"fraction": 0.70,
"fileName": "Applying file",
"largeFileMode": largeFileMode
]
)
let replaceSucceeded: Bool
if let storage = textStorage {
undoManager?.disableUndoRegistration()
storage.beginEditing()
storage.mutableString.replaceCharacters(in: safeSelection, with: content)
storage.endEditing()
undoManager?.enableUndoRegistration()
replaceSucceeded = true
} else {
// Fallback for environments where textStorage is temporarily unavailable.
let current = string as NSString
if safeSelection.location <= current.length &&
safeSelection.location + safeSelection.length <= current.length {
let replaced = current.replacingCharacters(in: safeSelection, with: content)
string = replaced
replaceSucceeded = true
} else {
replaceSucceeded = false
}
}
NotificationCenter.default.post(
name: .droppedFileLoadProgress,
object: nil,
userInfo: [
"fraction": replaceSucceeded ? 1.0 : 0.0,
"fileName": replaceSucceeded ? "Reading file" : "Import failed",
"largeFileMode": largeFileMode
]
)
completion(replaceSucceeded, replaceSucceeded ? total : 0)
}
private func clampedSelectionRange() -> NSRange {
clampedRange(selectedRange(), forTextLength: (string as NSString).length)
}
private func clampedRange(_ range: NSRange, forTextLength length: Int) -> NSRange {
guard length >= 0 else { return NSRange(location: 0, length: 0) }
if range.location == NSNotFound {
return NSRange(location: length, length: 0)
}
let safeLocation = min(max(0, range.location), length)
let maxLen = max(0, length - safeLocation)
let safeLength = min(max(0, range.length), maxLen)
return NSRange(location: safeLocation, length: safeLength)
}
private func readDroppedFileData(
at url: URL,
totalBytes: Int64,
progress: @escaping (Double) -> Void
) throws -> Data {
do {
let handle = try FileHandle(forReadingFrom: url)
defer { try? handle.close() }
var data = Data()
if totalBytes > 0, totalBytes <= Int64(Int.max) {
data.reserveCapacity(Int(totalBytes))
}
var loadedBytes: Int64 = 0
var lastReported: Double = -1
while true {
let chunk = try handle.read(upToCount: dropReadChunkSize) ?? Data()
if chunk.isEmpty { break }
data.append(chunk)
loadedBytes += Int64(chunk.count)
if totalBytes > 0 {
let fraction = min(1.0, Double(loadedBytes) / Double(totalBytes))
if fraction - lastReported >= 0.02 || fraction >= 1.0 {
lastReported = fraction
DispatchQueue.main.async {
progress(fraction)
}
}
}
}
if totalBytes > 0, lastReported < 1.0 {
DispatchQueue.main.async {
progress(1.0)
}
}
return data
} catch {
// Fallback path for URLs/FileHandle edge cases in sandboxed drag-drop.
let data = try Data(contentsOf: url, options: [.mappedIfSafe])
DispatchQueue.main.async {
progress(1.0)
}
return data
}
}
private func decodeDroppedFileText(_ data: Data, fileURL: URL) -> String {
let encodings: [String.Encoding] = [.utf8, .utf16, .utf16LittleEndian, .utf16BigEndian, .windowsCP1252, .isoLatin1]
for encoding in encodings {
if let decoded = String(data: data, encoding: encoding) {
return decoded
}
}
if let fallback = try? String(contentsOf: fileURL, encoding: .utf8) {
return fallback
}
return String(decoding: data, as: UTF8.self)
}
// MARK: - Typing helpers (existing behavior)
override func insertText(_ insertString: Any, replacementRange: NSRange) {
guard let s = insertString as? String else {
super.insertText(insertString, replacementRange: replacementRange)
@ -308,6 +506,7 @@ struct CustomTextEditor: NSViewRepresentable {
let colorScheme: ColorScheme
let fontSize: CGFloat
@Binding var isLineWrapEnabled: Bool
let isLargeFileMode: Bool
let translucentBackgroundEnabled: Bool
// Toggle soft-wrapping by adjusting text container sizing and scroller visibility.
@ -391,12 +590,12 @@ struct CustomTextEditor: NSViewRepresentable {
textView.delegate = context.coordinator
// Install line number ruler
scrollView.hasVerticalRuler = true
scrollView.rulersVisible = true
scrollView.hasVerticalRuler = !isLargeFileMode
scrollView.rulersVisible = !isLargeFileMode
scrollView.verticalRulerView = LineNumberRulerView(textView: textView)
// Apply wrapping and seed initial content
applyWrapMode(isWrapped: isLineWrapEnabled, textView: textView, scrollView: scrollView)
applyWrapMode(isWrapped: isLineWrapEnabled && !isLargeFileMode, textView: textView, scrollView: scrollView)
// Seed initial text
textView.string = text
@ -444,7 +643,9 @@ struct CustomTextEditor: NSViewRepresentable {
textView.drawsBackground = true
}
// Keep the text container width in sync & relayout
applyWrapMode(isWrapped: isLineWrapEnabled, textView: textView, scrollView: nsView)
nsView.hasVerticalRuler = !isLargeFileMode
nsView.rulersVisible = !isLargeFileMode
applyWrapMode(isWrapped: isLineWrapEnabled && !isLargeFileMode, textView: textView, scrollView: nsView)
// Force immediate reflow after toggling wrap
if let container = textView.textContainer, let lm = textView.layoutManager {
@ -539,6 +740,13 @@ struct CustomTextEditor: NSViewRepresentable {
return result
}()
if parent.isLargeFileMode {
self.lastHighlightedText = text
self.lastLanguage = lang
self.lastColorScheme = scheme
return
}
// Skip expensive highlighting for very large documents
let nsLen = (text as NSString).length
if nsLen > 200_000 { // ~200k UTF-16 code units
@ -643,6 +851,13 @@ struct CustomTextEditor: NSViewRepresentable {
NotificationCenter.default.post(name: .caretPositionDidChange, object: nil, userInfo: ["line": line, "column": col])
// Highlight current line
if parent.isLargeFileMode {
return
}
if ns.length > 300_000 {
// Large documents: skip full-range background rewrites to keep UI responsive.
return
}
let lineRange = ns.lineRange(for: NSRange(location: location, length: 0))
let fullRange = NSRange(location: 0, length: ns.length)
tv.textStorage?.beginEditing()
@ -775,6 +990,7 @@ struct CustomTextEditor: UIViewRepresentable {
let colorScheme: ColorScheme
let fontSize: CGFloat
@Binding var isLineWrapEnabled: Bool
let isLargeFileMode: Bool
let translucentBackgroundEnabled: Bool
func makeUIView(context: Context) -> LineNumberedTextViewContainer {
@ -790,10 +1006,15 @@ struct CustomTextEditor: UIViewRepresentable {
textView.smartQuotesType = .no
textView.smartInsertDeleteType = .no
textView.backgroundColor = translucentBackgroundEnabled ? .clear : .systemBackground
textView.textContainer.lineBreakMode = isLineWrapEnabled ? .byWordWrapping : .byClipping
textView.textContainer.widthTracksTextView = isLineWrapEnabled
textView.textContainer.lineBreakMode = (isLineWrapEnabled && !isLargeFileMode) ? .byWordWrapping : .byClipping
textView.textContainer.widthTracksTextView = isLineWrapEnabled && !isLargeFileMode
container.updateLineNumbers(for: text, fontSize: fontSize)
if isLargeFileMode {
container.lineNumberView.isHidden = true
} else {
container.lineNumberView.isHidden = false
container.updateLineNumbers(for: text, fontSize: fontSize)
}
context.coordinator.container = container
context.coordinator.textView = textView
context.coordinator.scheduleHighlightIfNeeded(currentText: text)
@ -810,9 +1031,14 @@ struct CustomTextEditor: UIViewRepresentable {
textView.font = UIFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
}
textView.backgroundColor = translucentBackgroundEnabled ? .clear : .systemBackground
textView.textContainer.lineBreakMode = isLineWrapEnabled ? .byWordWrapping : .byClipping
textView.textContainer.widthTracksTextView = isLineWrapEnabled
uiView.updateLineNumbers(for: text, fontSize: fontSize)
textView.textContainer.lineBreakMode = (isLineWrapEnabled && !isLargeFileMode) ? .byWordWrapping : .byClipping
textView.textContainer.widthTracksTextView = isLineWrapEnabled && !isLargeFileMode
if isLargeFileMode {
uiView.lineNumberView.isHidden = true
} else {
uiView.lineNumberView.isHidden = false
uiView.updateLineNumbers(for: text, fontSize: fontSize)
}
context.coordinator.syncLineNumberScroll()
context.coordinator.scheduleHighlightIfNeeded(currentText: text)
}
@ -842,6 +1068,13 @@ struct CustomTextEditor: UIViewRepresentable {
let lang = parent.language
let scheme = parent.colorScheme
if parent.isLargeFileMode {
lastHighlightedText = text
lastLanguage = lang
lastColorScheme = scheme
return
}
if text == lastHighlightedText && lang == lastLanguage && scheme == lastColorScheme {
return
}

View file

@ -51,6 +51,9 @@ class EditorViewModel: ObservableObject {
"yml": "yaml",
"xml": "xml",
"sql": "sql",
"log": "log",
"vim": "vim",
"ipynb": "ipynb",
"java": "java",
"kt": "kotlin",
"kts": "kotlin",
@ -67,7 +70,7 @@ class EditorViewModel: ObservableObject {
"cc": "cpp",
"hpp": "cpp",
"hh": "cpp",
"h": "c",
"h": "cpp",
//"cs": "csharp", // Removed this line as per instructions
"m": "objective-c",
"mm": "objective-c",

View file

@ -31,6 +31,9 @@ public struct LanguageDetector {
"yml": "yaml",
"xml": "xml",
"sql": "sql",
"log": "log",
"vim": "vim",
"ipynb": "ipynb",
"java": "java",
"kt": "kotlin",
"kts": "kotlin",
@ -47,7 +50,7 @@ public struct LanguageDetector {
"cc": "cpp",
"hpp": "cpp",
"hh": "cpp",
"h": "c",
"h": "cpp",
"m": "objective-c",
"mm": "objective-c",
"cs": "csharp",
@ -71,6 +74,7 @@ public struct LanguageDetector {
".bash_login": "bash",
".bash_logout": "bash",
".profile": "bash",
".vimrc": "vim",
".env": "ini",
".gitconfig": "ini"
]

View file

@ -179,7 +179,7 @@ struct NeonVisionEditorApp: App {
}
CommandMenu("Language") {
ForEach(["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in
ForEach(["swift", "python", "javascript", "typescript", "php", "java", "kotlin", "go", "ruby", "rust", "sql", "html", "css", "cpp", "csharp", "objective-c", "json", "xml", "yaml", "toml", "csv", "ini", "vim", "log", "ipynb", "markdown", "bash", "zsh", "powershell", "standard", "plain"], id: \.self) { lang in
let label: String = {
switch lang {
case "php": return "PHP"
@ -193,6 +193,9 @@ struct NeonVisionEditorApp: App {
case "csv": return "CSV"
case "ini": return "INI"
case "sql": return "SQL"
case "vim": return "Vim"
case "log": return "Log"
case "ipynb": return "Jupyter Notebook"
case "html": return "HTML"
case "css": return "CSS"
case "standard": return "Standard"

View file

@ -371,6 +371,10 @@ extension Notification.Name {
static let showWelcomeTourRequested = Notification.Name("showWelcomeTourRequested")
static let toggleVimModeRequested = Notification.Name("toggleVimModeRequested")
static let vimModeStateDidChange = Notification.Name("vimModeStateDidChange")
static let droppedFileURL = Notification.Name("droppedFileURL")
static let droppedFileLoadStarted = Notification.Name("droppedFileLoadStarted")
static let droppedFileLoadProgress = Notification.Name("droppedFileLoadProgress")
static let droppedFileLoadFinished = Notification.Name("droppedFileLoadFinished")
static let toggleSidebarRequested = Notification.Name("toggleSidebarRequested")
static let toggleBrainDumpModeRequested = Notification.Name("toggleBrainDumpModeRequested")
static let toggleLineWrapRequested = Notification.Name("toggleLineWrapRequested")

View file

@ -288,6 +288,29 @@ func getSyntaxPatterns(for language: String, colors: SyntaxColors) -> [String: C
#"^;.*$"#: colors.comment,
#"^\w+\s*=\s*.*$"#: colors.property
]
case "vim":
return [
#"\b(set|let|if|endif|for|endfor|while|endwhile|function|endfunction|command|autocmd|syntax|highlight|nnoremap|inoremap|vnoremap|map|nmap|imap|vmap)\b"#: colors.keyword,
#"\$[A-Za-z_][A-Za-z0-9_]*|[gbwtslv]:[A-Za-z_][A-Za-z0-9_]*"#: colors.variable,
#"\"[^\"]*\"|'[^']*'"#: colors.string,
#"^\s*\".*$"#: colors.comment,
#"\b[0-9]+\b"#: colors.number
]
case "log":
return [
#"\b(ERROR|ERR|FATAL|WARN|WARNING|INFO|DEBUG|TRACE)\b"#: colors.keyword,
#"\b[0-9]{4}-[0-9]{2}-[0-9]{2}[ T][0-9:.+-Z]+\b"#: colors.meta,
#"\b([0-9]+(\.[0-9]+)?)\b"#: colors.number,
#"(Exception|Traceback|Caused by:).*"#: colors.attribute
]
case "ipynb":
return [
#"\"(cells|metadata|source|outputs|execution_count|cell_type|kernelspec|language_info)\"\s*:"#: colors.property,
#"\"([^\"\\]|\\.)*\""#: colors.string,
#"\b(-?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?)\b"#: colors.number,
#"\b(true|false|null)\b"#: colors.keyword,
#"[{}\[\],:]"#: colors.meta
]
case "csharp":
return [
#"\b(class|interface|enum|struct|namespace|using|public|private|protected|internal|static|readonly|sealed|abstract|virtual|override|async|await|new|return|if|else|for|foreach|while|do|switch|case|break|continue|try|catch|finally|throw)\b"#: colors.keyword,

View file

@ -117,6 +117,10 @@ If macOS blocks first launch:
- Unified persistence behavior for Brain Dump and translucent window toggles.
- Removed duplicate `Cmd+F` binding conflict in toolbar wiring.
- Verified command-system changes on macOS and iOS simulator builds.
- Added syntax highlighting support for `vim`, `log`, and `ipynb` files.
- Added extension-based auto-detection for `.vim`, `.log`, `.ipynb`, and `.vimrc`.
- Improved header file default highlighting by mapping `.h` to `cpp`.
- Added language picker entries for **Vim**, **Log**, and **Jupyter Notebook**.
Full release history: [`CHANGELOG.md`](CHANGELOG.md)