mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
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:
parent
8f1e181187
commit
4293981992
11 changed files with 459 additions and 61 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue