diff --git a/Neon Vision Editor.xcodeproj/project.pbxproj b/Neon Vision Editor.xcodeproj/project.pbxproj
index e2f8554..96bc9d6 100644
--- a/Neon Vision Editor.xcodeproj/project.pbxproj
+++ b/Neon Vision Editor.xcodeproj/project.pbxproj
@@ -259,7 +259,7 @@
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 25;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;
@@ -296,7 +296,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.0;
- MARKETING_VERSION = 1.0;
+ MARKETING_VERSION = 0.2;
PRODUCT_BUNDLE_IDENTIFIER = "h3p.Neon-Vision-Editor";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -330,7 +330,7 @@
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
+ CURRENT_PROJECT_VERSION = 25;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;
@@ -367,7 +367,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 26.0;
- MARKETING_VERSION = 1.0;
+ MARKETING_VERSION = 0.2;
PRODUCT_BUNDLE_IDENTIFIER = "h3p.Neon-Vision-Editor";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
diff --git a/Neon Vision Editor.xcodeproj/xcshareddata/xcschemes/Neon Vision Editor.xcscheme b/Neon Vision Editor.xcodeproj/xcshareddata/xcschemes/Neon Vision Editor.xcscheme
new file mode 100644
index 0000000..ee839cb
--- /dev/null
+++ b/Neon Vision Editor.xcodeproj/xcshareddata/xcschemes/Neon Vision Editor.xcscheme
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Neon Vision Editor.xcodeproj/xcuserdata/h3p.xcuserdatad/xcschemes/xcschememanagement.plist b/Neon Vision Editor.xcodeproj/xcuserdata/h3p.xcuserdatad/xcschemes/xcschememanagement.plist
index 08b3943..b6ff149 100644
--- a/Neon Vision Editor.xcodeproj/xcuserdata/h3p.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Neon Vision Editor.xcodeproj/xcuserdata/h3p.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -10,5 +10,13 @@
0
+ SuppressBuildableAutocreation
+
+ 98EAE6322E5F15E80050E579
+
+ primary
+
+
+
diff --git a/Neon Vision Editor/ContentView.swift b/Neon Vision Editor/ContentView.swift
index ecfc14a..9a09579 100644
--- a/Neon Vision Editor/ContentView.swift
+++ b/Neon Vision Editor/ContentView.swift
@@ -1,8 +1,13 @@
-// FIXES APPLIED: Consistent rename and content persistence for tab creation and language updates
+// Simplified: Single-document editor without tabs; removed AIModel references and fixed compile errors
import SwiftUI
-<<<<<<< HEAD
import AppKit
+enum AIModel: String, CaseIterable, Identifiable {
+ case appleIntelligence
+ case grok
+ var id: String { rawValue }
+}
+
// Extension to calculate string width
extension String {
func width(usingFont font: NSFont) -> CGFloat {
@@ -17,23 +22,22 @@ struct ContentView: View {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.showGrokError) private var showGrokError
@Environment(\.grokErrorMessage) private var grokErrorMessage
- @Environment(\.selectedAIModel) private var selectedAIModel
-
+
+ // Fallback single-document state in case the view model doesn't expose one
+ @State private var selectedModel: AIModel = .appleIntelligence
@State private var singleContent: String = ""
@State private var singleLanguage: String = "swift"
-
+ @State private var caretStatus: String = "Ln 1, Col 1"
+ @State private var editorFontSize: CGFloat = 14
+
var body: some View {
NavigationSplitView {
sidebarView
} detail: {
editorView
}
+ .navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 600)
.frame(minWidth: 600, minHeight: 400)
- .background(.ultraThinMaterial)
- .overlay(.ultraThinMaterial.opacity(0.2)) // Fallback for liquidGlassEffect
- .sheet(isPresented: $viewModel.showingRename) {
- renameSheet
- }
.alert("AI Error", isPresented: showGrokError) {
Button("OK") { }
} message: {
@@ -41,62 +45,110 @@ struct ContentView: View {
}
.navigationTitle("NeonVision Editor")
}
-
+
@ViewBuilder
private var sidebarView: some View {
if viewModel.showSidebar && !viewModel.isBrainDumpMode {
- SidebarView(content: (viewModel.selectedTab?.content ?? singleContent),
- language: (viewModel.selectedTab?.language ?? singleLanguage))
- .frame(minWidth: 200, idealWidth: 250)
- .background(.ultraThinMaterial)
- .overlay(.ultraThinMaterial.opacity(0.2))
+ SidebarView(content: currentContent,
+ language: currentLanguage)
+ .frame(minWidth: 200, idealWidth: 250, maxWidth: 600)
.animation(.spring(), value: viewModel.showSidebar)
.safeAreaInset(edge: .bottom) {
Divider()
}
}
}
-
+
+ private var currentContentBinding: Binding {
+ if let tab = viewModel.selectedTab {
+ return Binding(
+ get: { tab.content },
+ set: { newValue in viewModel.updateTabContent(tab: tab, content: newValue) }
+ )
+ } else {
+ return $singleContent
+ }
+ }
+
+ private var currentLanguageBinding: Binding {
+ if let selectedID = viewModel.selectedTabID, let idx = viewModel.tabs.firstIndex(where: { $0.id == selectedID }) {
+ return Binding(
+ get: { viewModel.tabs[idx].language },
+ set: { newValue in viewModel.tabs[idx].language = newValue }
+ )
+ } else {
+ return $singleLanguage
+ }
+ }
+
+ private var currentContent: String { currentContentBinding.wrappedValue }
+ private var currentLanguage: String { currentLanguageBinding.wrappedValue }
+
@ViewBuilder
private var editorView: some View {
VStack(spacing: 0) {
- tabViewContent
+ // Single editor (no TabView)
+ CustomTextEditor(
+ text: currentContentBinding,
+ language: currentLanguage,
+ colorScheme: colorScheme,
+ fontSize: editorFontSize,
+ isLineWrapEnabled: $viewModel.isLineWrapEnabled
+ )
+ .frame(maxWidth: viewModel.isBrainDumpMode ? 800 : .infinity)
+ .frame(maxHeight: .infinity)
+ .padding(.horizontal, viewModel.isBrainDumpMode ? 100 : 0)
+ .padding(.vertical, viewModel.isBrainDumpMode ? 40 : 0)
+
if !viewModel.isBrainDumpMode {
wordCountView
}
}
+ .onReceive(NotificationCenter.default.publisher(for: .caretPositionDidChange)) { notif in
+ if let line = notif.userInfo?["line"] as? Int, let col = notif.userInfo?["column"] as? Int {
+ caretStatus = "Ln \(line), Col \(col)"
+ }
+ }
.toolbar {
- ToolbarItem(placement: .primaryAction) {
- Picker("Language", selection: Binding(
- get: {
- viewModel.selectedTab?.language ?? singleLanguage
- },
- set: { newLang in
- if let selectedID = viewModel.selectedTabID, let idx = viewModel.tabs.firstIndex(where: { $0.id == selectedID }) {
- viewModel.tabs[idx].language = newLang
- } else {
- singleLanguage = newLang
+ ToolbarItemGroup(placement: .primaryAction) {
+ HStack(spacing: 8) {
+ Picker("AI Model", selection: $selectedModel) {
+ Text("Apple Intelligence").tag(AIModel.appleIntelligence)
+ Text("Grok").tag(AIModel.grok)
+ }
+ .labelsHidden()
+ .controlSize(.large)
+ .frame(width: 170)
+ .padding(.vertical, 2)
+
+ Picker("Language", selection: currentLanguageBinding) {
+ ForEach(["swift", "python", "javascript", "html", "css", "c", "cpp", "json", "markdown"], id: \.self) { lang in
+ Text(lang.capitalized).tag(lang)
}
}
- )) {
- ForEach(["swift", "python", "javascript", "html", "css", "c", "cpp", "json", "markdown"], id: \.self) { lang in
- Text(lang.capitalized).tag(lang)
+ .labelsHidden()
+ .controlSize(.large)
+ .frame(width: 140)
+ .padding(.vertical, 2)
+
+ Divider()
+ Button(action: { editorFontSize = max(8, editorFontSize - 1) }) {
+ Image(systemName: "textformat.size.smaller")
}
+ .help("Decrease Font Size")
+ Button(action: { editorFontSize = min(48, editorFontSize + 1) }) {
+ Image(systemName: "textformat.size.larger")
+ }
+ .help("Increase Font Size")
}
- .frame(width: 150)
- }
- ToolbarItem(placement: .primaryAction) {
- Picker("AI Model", selection: selectedAIModel) {
- Text("Apple Intelligence").tag(AIModel.appleIntelligence)
- Text("Grok").tag(AIModel.grok)
- }
- .frame(width: 150)
}
ToolbarItemGroup(placement: .automatic) {
Button(action: { viewModel.openFile() }) {
Image(systemName: "folder")
}
- Button(action: { if let tab = viewModel.selectedTab { viewModel.saveFile(tab: tab) } }) {
+ Button(action: {
+ if let tab = viewModel.selectedTab { viewModel.saveFile(tab: tab) }
+ }) {
Image(systemName: "square.and.arrow.down")
}
.disabled(viewModel.selectedTab == nil)
@@ -108,289 +160,81 @@ struct ContentView: View {
}
}
}
- .frame(maxWidth: .infinity, maxHeight: .infinity) // Added to expand editorView
+ .toolbarBackground(.visible, for: .windowToolbar)
+ .toolbarBackground(Color(nsColor: .windowBackgroundColor), for: .windowToolbar)
}
-
- @ViewBuilder
- private var tabViewContent: some View {
- VStack(spacing: 0) {
- CustomTextEditor(
- text: Binding(
- get: {
- if let selID = viewModel.selectedTabID, let tab = viewModel.tabs.first(where: { $0.id == selID }) {
- return tab.content
- } else {
- return singleContent
- }
- },
- set: { newValue in
- if let selID = viewModel.selectedTabID, let tab = viewModel.tabs.first(where: { $0.id == selID }) {
- viewModel.updateTabContent(tab: tab, content: newValue)
- } else {
- singleContent = newValue
- }
- }
- ),
- language: viewModel.selectedTab?.language ?? singleLanguage,
- colorScheme: colorScheme
- )
- .frame(maxWidth: viewModel.isBrainDumpMode ? 800 : .infinity)
- .padding(viewModel.isBrainDumpMode ? .horizontal : [], 100)
-=======
-import AppKit // Added for NSFont and NSAttributedString
-
-// Extension to calculate string width
-extension String {
- func width(usingFont font: NSFont) -> CGFloat {
- let attributes = [NSAttributedString.Key.font: font]
- let size = (self as NSString).size(withAttributes: attributes)
- return size.width
- }
-}
-
-struct ContentView: View {
- @EnvironmentObject private var viewModel: EditorViewModel
- @Environment(\.colorScheme) private var colorScheme
-
- var body: some View {
- NavigationSplitView {
- // Sidebar
- if viewModel.showSidebar && !viewModel.isBrainDumpMode {
- SidebarView(content: viewModel.selectedTab?.content ?? "",
- language: viewModel.selectedTab?.language ?? "swift")
- .frame(minWidth: 200)
- .toolbar {
- ToolbarItem {
- Picker("Language", selection: Binding(
- get: { viewModel.selectedTab?.language ?? "swift" },
- set: { if let tab = viewModel.selectedTab { viewModel.updateTabLanguage(tab: tab, language: $0) }
- })) {
- ForEach(["swift", "python", "javascript", "html", "css", "c", "cpp", "json", "markdown"], id: \.self) { lang in
- Text(lang.capitalized).tag(lang)
- }
- }
- }
- }
- }
- } detail: {
- // Main Editor with Tabs
- VStack {
- if !viewModel.tabs.isEmpty {
- TabView(selection: $viewModel.selectedTabID) {
- ForEach(viewModel.tabs) { tab in
- HighlightedTextEditor(
- text: Binding(
- get: { tab.content },
- set: { viewModel.updateTabContent(tab: tab, content: $0) }
- ),
- language: tab.language,
- colorScheme: colorScheme
- )
- .frame(maxWidth: viewModel.isBrainDumpMode ? 800 : .infinity)
- .padding(viewModel.isBrainDumpMode ? .horizontal : [], 100)
- .tabItem {
- Text(tab.name + (tab.fileURL == nil && !tab.content.isEmpty ? " *" : ""))
- }
- .tag(tab.id)
- }
- }
-
- if !viewModel.isBrainDumpMode {
- HStack {
- Spacer()
- Text("Words: \(viewModel.wordCount(for: viewModel.selectedTab?.content ?? ""))")
- .foregroundColor(.secondary)
- .padding(.bottom, 8)
- .padding(.trailing, 8)
- }
- }
- } else {
- Text("Select a tab or create a new one")
- .foregroundColor(.secondary)
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- }
- }
- .toolbar {
- ToolbarItemGroup {
- Button(action: { viewModel.addNewTab(); viewModel.showingRename = true; viewModel.renameText = viewModel.selectedTab?.name ?? "Untitled" }) {
- Image(systemName: "plus")
- }
- Button(action: { viewModel.openFile() }) {
- Image(systemName: "folder")
- }
- Button(action: { if let tab = viewModel.selectedTab { viewModel.saveFile(tab: tab) } }) {
- Image(systemName: "square.and.arrow.down")
- }
- .disabled(viewModel.selectedTab == nil)
- Button(action: { viewModel.showSidebar.toggle() }) {
- Image(systemName: viewModel.showSidebar ? "sidebar.left" : "sidebar.right")
- }
- Button(action: { viewModel.isBrainDumpMode.toggle() }) {
- Image(systemName: "note.text")
- }
- }
- }
- }
- .frame(minWidth: 600, minHeight: 400)
- .background(.ultraThinMaterial)
- .sheet(isPresented: $viewModel.showingRename) {
- VStack {
- Text("Rename Tab")
- .font(.headline)
- TextField("Name", text: $viewModel.renameText)
- .textFieldStyle(.roundedBorder)
- .padding()
- HStack {
- Button("Cancel") { viewModel.showingRename = false }
- Button("OK") {
- if let tab = viewModel.selectedTab {
- viewModel.renameTab(tab: tab, newName: viewModel.renameText)
- }
- viewModel.showingRename = false
- }
- .disabled(viewModel.renameText.isEmpty)
- }
- }
- .padding()
- .frame(width: 300)
- }
- .navigationTitle(viewModel.selectedTab?.name ?? "NeonVision Editor")
- }
-}
-
-struct SidebarView: View {
- let content: String
- let language: String
- @State private var selectedTOCItem: String?
-
- var body: some View {
- List(generateTableOfContents(), id: \.self, selection: $selectedTOCItem) { item in
- Text(item)
- .foregroundColor(.secondary)
- .padding(.vertical, 2)
- .onTapGesture {
- if let lineNumber = lineNumber(for: item) {
- NotificationCenter.default.post(name: .moveCursorToLine, object: lineNumber)
- }
- }
->>>>>>> main
- }
- .listStyle(.sidebar)
- }
-
-<<<<<<< HEAD
@ViewBuilder
private var wordCountView: some View {
HStack {
Spacer()
- Text("Words: \(viewModel.wordCount(for: viewModel.selectedTab?.content ?? ""))")
+ Text("\(caretStatus) • Words: \(viewModel.wordCount(for: currentContent))")
.font(.system(size: 12))
.foregroundColor(.secondary)
.padding(.bottom, 8)
.padding(.trailing, 16)
}
}
-
- @ViewBuilder
- private var renameSheet: some View {
- VStack {
- Text("Rename Tab")
- .font(.headline)
- TextField("Name", text: $viewModel.renameText)
- .textFieldStyle(.roundedBorder)
- .padding()
- HStack(spacing: 12) {
- Button("Cancel") {
- viewModel.showingRename = false
- }
- .buttonStyle(.bordered)
-
- Button("OK") {
- // Ensure we have a selected tab; if not, select the first available tab
- if viewModel.selectedTab == nil, let first = viewModel.tabs.first {
- viewModel.selectedTabID = first.id
- }
- if let tab = viewModel.selectedTab {
- if viewModel.selectedTabID != tab.id {
- viewModel.selectedTabID = tab.id
- }
- viewModel.renameTab(tab: tab, newName: viewModel.renameText)
- }
- viewModel.showingRename = false
- }
- .buttonStyle(.borderedProminent)
- .disabled(viewModel.renameText.isEmpty)
- }
- }
- .padding()
- .frame(width: 320)
- .background(.regularMaterial)
- .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
- .shadow(radius: 12)
- .interactiveDismissDisabled(false)
- .allowsHitTesting(true)
- }
}
struct SidebarView: View {
let content: String
let language: String
@State private var selectedTOCItem: String?
-
+
var body: some View {
List(generateTableOfContents(), id: \.self, selection: $selectedTOCItem) { item in
- Text(item)
- .font(.system(size: 13))
- .foregroundColor(.primary)
- .padding(.vertical, 4)
- .padding(.horizontal, 8)
- .onTapGesture {
- if let lineNumber = lineNumber(for: item) {
- NotificationCenter.default.post(name: .moveCursorToLine, object: lineNumber)
+ Button(action: {
+ // Expect item format: "... (Line N)"
+ if let startRange = item.range(of: "(Line "),
+ let endRange = item.range(of: ")", range: startRange.upperBound.. 0 {
+ DispatchQueue.main.async {
+ NotificationCenter.default.post(name: .moveCursorToLine, object: lineOneBased)
+ }
}
}
+ }) {
+ Text(item)
+ .font(.system(size: 13))
+ .foregroundColor(.primary)
+ .padding(.vertical, 4)
+ .padding(.horizontal, 8)
+ .tag(item)
+ }
+ .buttonStyle(.plain)
}
.listStyle(.sidebar)
.frame(maxWidth: .infinity, alignment: .leading)
+ .onChange(of: selectedTOCItem) { oldValue, newValue in
+ guard let item = newValue else { return }
+ if let startRange = item.range(of: "(Line "),
+ let endRange = item.range(of: ")", range: startRange.upperBound.. 0 {
+ DispatchQueue.main.async {
+ NotificationCenter.default.post(name: .moveCursorToLine, object: lineOneBased)
+ }
+ }
+ }
+ }
}
-
+
func generateTableOfContents() -> [String] {
guard !content.isEmpty else { return ["No content available"] }
let lines = content.components(separatedBy: .newlines)
var toc: [String] = []
-
+
switch language {
case "swift":
toc = lines.enumerated().compactMap { index, line in
let trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed.hasPrefix("func ") || trimmed.hasPrefix("struct ") ||
trimmed.hasPrefix("class ") || trimmed.hasPrefix("enum ") {
-=======
- func generateTableOfContents() -> [String] {
- if content.isEmpty {
- return ["No content"]
- }
- let lines = content.components(separatedBy: .newlines)
- switch language {
- case "swift":
- return lines.enumerated().compactMap { index, line in
- let trimmed = line.trimmingCharacters(in: .whitespaces)
- if trimmed.hasPrefix("func") || trimmed.hasPrefix("struct") ||
- trimmed.hasPrefix("class") || trimmed.hasPrefix("enum") {
return "\(trimmed) (Line \(index + 1))"
}
return nil
}
- case "python":
- return lines.enumerated().compactMap { index, line in
- let trimmed = line.trimmingCharacters(in: .whitespaces)
- if trimmed.hasPrefix("def") || trimmed.hasPrefix("class") {
->>>>>>> main
- return "\(trimmed) (Line \(index + 1))"
- }
- return nil
- }
-<<<<<<< HEAD
case "python":
toc = lines.enumerated().compactMap { index, line in
let trimmed = line.trimmingCharacters(in: .whitespaces)
@@ -403,36 +247,20 @@ struct SidebarView: View {
toc = lines.enumerated().compactMap { index, line in
let trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed.hasPrefix("function ") || trimmed.hasPrefix("class ") {
-=======
- case "javascript":
- return lines.enumerated().compactMap { index, line in
- let trimmed = line.trimmingCharacters(in: .whitespaces)
- if trimmed.hasPrefix("function") || trimmed.hasPrefix("class") {
->>>>>>> main
return "\(trimmed) (Line \(index + 1))"
}
return nil
}
case "c", "cpp":
-<<<<<<< HEAD
toc = lines.enumerated().compactMap { index, line in
let trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed.contains("(") && !trimmed.contains(";") && (trimmed.hasPrefix("void ") || trimmed.hasPrefix("int ") || trimmed.hasPrefix("float ") || trimmed.hasPrefix("double ") || trimmed.hasPrefix("char ") || trimmed.contains("{")) {
-=======
- return lines.enumerated().compactMap { index, line in
- let trimmed = line.trimmingCharacters(in: .whitespaces)
- if trimmed.contains("(") && !trimmed.contains(";") {
->>>>>>> main
return "\(trimmed) (Line \(index + 1))"
}
return nil
}
case "html", "css", "json", "markdown":
-<<<<<<< HEAD
toc = lines.enumerated().compactMap { index, line in
-=======
- return lines.enumerated().compactMap { index, line in
->>>>>>> main
let trimmed = line.trimmingCharacters(in: .whitespaces)
if !trimmed.isEmpty && (trimmed.hasPrefix("#") || trimmed.hasPrefix(">>>>>> main
}
-
+
return toc.isEmpty ? ["No headers found"] : toc
}
-
- func lineNumber(for item: String) -> Int? {
- let lines = content.components(separatedBy: .newlines)
- return lines.firstIndex { $0.trimmingCharacters(in: .whitespaces) == item.components(separatedBy: " (Line").first }
-<<<<<<< HEAD
+}
+
+final class AcceptingTextView: NSTextView {
+ override var acceptsFirstResponder: Bool { true }
+ override var mouseDownCanMoveWindow: Bool { false }
+ override var isOpaque: Bool { false }
+
+ override func insertText(_ insertString: Any, replacementRange: NSRange) {
+ guard let s = insertString as? String else {
+ super.insertText(insertString, replacementRange: replacementRange)
+ return
+ }
+ if s == "\n" {
+ // Auto-indent: copy leading whitespace from current line
+ let ns = (string as NSString)
+ let sel = selectedRange()
+ let lineRange = ns.lineRange(for: NSRange(location: sel.location, length: 0))
+ let currentLine = ns.substring(with: NSRange(location: lineRange.location, length: sel.location - lineRange.location))
+ let indent = currentLine.prefix { $0 == " " || $0 == "\t" }
+ super.insertText("\n" + indent, replacementRange: replacementRange)
+ return
+ }
+ // Bracket/quote pairing
+ let pairs: [String: String] = ["(": ")", "[": "]", "{": "}", "\"": "\"", "'": "'"]
+ if let closing = pairs[s] {
+ let sel = selectedRange()
+ super.insertText(s + closing, replacementRange: replacementRange)
+ setSelectedRange(NSRange(location: sel.location + 1, length: 0))
+ return
+ }
+ super.insertText(insertString, replacementRange: replacementRange)
}
}
@@ -461,131 +311,318 @@ struct CustomTextEditor: NSViewRepresentable {
@Binding var text: String
let language: String
let colorScheme: ColorScheme
-
+ let fontSize: CGFloat
+ @Binding var isLineWrapEnabled: Bool
+
+ private func applyWrapMode(isWrapped: Bool, textView: NSTextView, scrollView: NSScrollView) {
+ if isWrapped {
+ // Wrap: track the text view width, no horizontal scrolling
+ textView.isHorizontallyResizable = false
+ textView.textContainer?.widthTracksTextView = true
+ textView.textContainer?.heightTracksTextView = false
+ scrollView.hasHorizontalScroller = false
+ // Ensure the container width matches the visible content width
+ let contentWidth = scrollView.contentSize.width
+ let width = contentWidth > 0 ? contentWidth : scrollView.frame.size.width
+ textView.textContainer?.containerSize = NSSize(width: width, height: CGFloat.greatestFiniteMagnitude)
+ } else {
+ // No wrap: allow horizontal expansion and horizontal scrolling
+ textView.isHorizontallyResizable = true
+ textView.textContainer?.widthTracksTextView = false
+ textView.textContainer?.heightTracksTextView = false
+ scrollView.hasHorizontalScroller = true
+ textView.textContainer?.containerSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
+ }
+ }
+
func makeNSView(context: Context) -> NSScrollView {
- // Use AppKit's factory to get a properly configured scroll view + text view
- let scrollView = NSTextView.scrollableTextView()
- let textView = scrollView.documentView as! NSTextView
+ // Use AppKit's factory to get a correctly configured scrollable plain text editor
+ let scrollView = NSTextView.scrollablePlainDocumentContentTextView()
+ scrollView.drawsBackground = false
+ scrollView.autohidesScrollers = true
+ scrollView.hasVerticalScroller = true
+ scrollView.contentView.postsBoundsChangedNotifications = true
- // Configure text view
+ guard let textView = scrollView.documentView as? NSTextView else {
+ return scrollView
+ }
+
+ // Configure the text view
textView.isEditable = true
- textView.isRulerVisible = false
- textView.font = NSFont.monospacedSystemFont(ofSize: 14, weight: .regular)
- textView.backgroundColor = .clear
- textView.textContainerInset = NSSize(width: 12, height: 12)
- textView.textColor = .labelColor
-
- // Plain text configuration and initial value
textView.isRichText = false
- textView.usesRuler = false
textView.usesFindBar = true
- textView.allowsUndo = true
- textView.isAutomaticQuoteSubstitutionEnabled = false
- textView.isAutomaticDataDetectionEnabled = false
- textView.isAutomaticLinkDetectionEnabled = false
- textView.string = self.text
-
- // Disable smart replacements/spell checking for code
- textView.textContainer?.lineFragmentPadding = 0
- textView.isAutomaticTextReplacementEnabled = false
- textView.isAutomaticSpellingCorrectionEnabled = false
-
- // Sizing behavior: allow vertical growth and wrap to width
- textView.minSize = NSSize(width: 0, height: 0)
- textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
textView.isVerticallyResizable = true
textView.isHorizontallyResizable = false
- textView.autoresizingMask = [.width]
- textView.postsFrameChangedNotifications = true
+ textView.font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
+ textView.backgroundColor = .textBackgroundColor
+ textView.textContainerInset = NSSize(width: 12, height: 12)
+ textView.minSize = NSSize(width: 0, height: 0)
+ textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
+ textView.isSelectable = true
+ textView.allowsUndo = true
+ textView.textColor = .labelColor
+ textView.insertionPointColor = .controlAccentColor
+ textView.drawsBackground = true
+ textView.isAutomaticTextCompletionEnabled = false
- if let container = textView.textContainer {
- container.containerSize = NSSize(width: scrollView.contentSize.width, height: .greatestFiniteMagnitude)
- container.widthTracksTextView = true
- container.heightTracksTextView = false
- }
+ // Disable smart substitutions/detections that can interfere with selection when recoloring
+ textView.isAutomaticQuoteSubstitutionEnabled = false
+ textView.isAutomaticDashSubstitutionEnabled = false
+ textView.isAutomaticDataDetectionEnabled = false
+ textView.isAutomaticLinkDetectionEnabled = false
+ textView.isGrammarCheckingEnabled = false
+ textView.isContinuousSpellCheckingEnabled = false
+ textView.smartInsertDeleteEnabled = false
- // Configure scroll view
- scrollView.hasVerticalScroller = true
- scrollView.hasHorizontalScroller = false
- scrollView.autohidesScrollers = true
- scrollView.borderType = .noBorder
- scrollView.backgroundColor = .clear
-
- // Keep container width in sync with scroll view size changes
- scrollView.contentView.postsBoundsChangedNotifications = true
- NotificationCenter.default.addObserver(context.coordinator,
- selector: #selector(context.coordinator.scrollViewBoundsDidChange(_:)),
- name: NSView.boundsDidChangeNotification,
- object: scrollView.contentView)
-
- // Coordinator and notifications
textView.delegate = context.coordinator
- NotificationCenter.default.addObserver(context.coordinator, selector: #selector(context.coordinator.updateTextContainerSize), name: NSView.frameDidChangeNotification, object: textView)
- context.coordinator.textView = textView
- // Apply initial syntax highlighting
- context.coordinator.applySyntaxHighlighting()
+ // Add line number ruler
+ scrollView.hasVerticalRuler = true
+ scrollView.rulersVisible = true
+ scrollView.verticalRulerView = LineNumberRulerView(textView: textView)
- DispatchQueue.main.async {
- textView.window?.makeFirstResponder(textView)
+ // Apply wrapping mode configuration
+ applyWrapMode(isWrapped: isLineWrapEnabled, textView: textView, scrollView: scrollView)
+
+ // Seed initial text
+ textView.string = text
+ DispatchQueue.main.async { [weak scrollView, weak textView] in
+ guard let sv = scrollView, let tv = textView else { return }
+ sv.window?.makeFirstResponder(tv)
}
+ context.coordinator.scheduleHighlightIfNeeded(currentText: text)
- return scrollView
- }
-
- func updateNSView(_ nsView: NSScrollView, context: Context) {
- if let textView = nsView.documentView as? NSTextView {
- // Only push SwiftUI -> AppKit when the source of truth changed
- if textView.string != self.text {
- textView.string = self.text
- context.coordinator.applySyntaxHighlighting()
- }
- // Do not write back here. Coordinator's textDidChange handles AppKit -> SwiftUI updates.
-
- if let container = textView.textContainer {
- let width = nsView.contentSize.width
- if container.containerSize.width != width {
- container.containerSize = NSSize(width: width, height: .greatestFiniteMagnitude)
- container.widthTracksTextView = true
+ NotificationCenter.default.addObserver(forName: NSView.boundsDidChangeNotification, object: scrollView.contentView, queue: .main) { [weak textView, weak scrollView] _ in
+ guard let tv = textView, let sv = scrollView else { return }
+ if tv.textContainer?.widthTracksTextView == true {
+ tv.textContainer?.containerSize.width = sv.contentSize.width
+ if let container = tv.textContainer {
+ tv.layoutManager?.ensureLayout(for: container)
}
}
+ }
+
+ context.coordinator.textView = textView
+ return scrollView
+ }
+
+ func updateNSView(_ nsView: NSScrollView, context: Context) {
+ if let textView = nsView.documentView as? NSTextView {
+ if textView.string != text {
+ textView.string = text
+ }
+ if textView.font?.pointSize != fontSize {
+ textView.font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
+ }
+ // Keep the text container width in sync & relayout
+ applyWrapMode(isWrapped: isLineWrapEnabled, textView: textView, scrollView: nsView)
+ if let container = textView.textContainer {
+ textView.layoutManager?.ensureLayout(for: container)
+ }
textView.invalidateIntrinsicContentSize()
- textView.layoutManager?.ensureLayout(for: textView.textContainer!)
+ // Only schedule highlight if needed (e.g., language/color scheme changes or external text updates)
+ context.coordinator.scheduleHighlightIfNeeded()
}
}
-
+
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
-
+
class Coordinator: NSObject, NSTextViewDelegate {
var parent: CustomTextEditor
weak var textView: NSTextView?
-
+
+ private let highlightQueue = DispatchQueue(label: "NeonVision.SyntaxHighlight", qos: .userInitiated)
+ private var pendingHighlight: DispatchWorkItem?
+ private var lastHighlightedText: String = ""
+ private var lastLanguage: String?
+ private var lastColorScheme: ColorScheme?
+
init(_ parent: CustomTextEditor) {
self.parent = parent
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(moveToLine(_:)), name: .moveCursorToLine, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(streamSuggestion(_:)), name: .streamSuggestion, object: nil)
}
-
- @objc func moveToLine(_ notification: Notification) {
- guard let lineNumber = notification.object as? Int,
- let textView = textView,
- !parent.text.isEmpty else { return }
-
- let lines = parent.text.components(separatedBy: .newlines)
- guard lineNumber >= 0 && lineNumber < lines.count else { return }
-
- let lineStart = lines[0.. 0 ? 1 : 0)
- textView.setSelectedRange(NSRange(location: lineStart, length: 0))
- textView.scrollRangeToVisible(NSRange(location: lineStart, length: 0))
+
+ deinit {
+ NotificationCenter.default.removeObserver(self)
}
-
+
+ func scheduleHighlightIfNeeded(currentText: String? = nil) {
+ guard textView != nil else { return }
+ // Defer highlighting while a modal panel is presented (e.g., NSSavePanel)
+ if NSApp.modalWindow != nil {
+ pendingHighlight?.cancel()
+ let work = DispatchWorkItem { [weak self] in
+ self?.scheduleHighlightIfNeeded(currentText: currentText)
+ }
+ pendingHighlight = work
+ highlightQueue.asyncAfter(deadline: .now() + 0.3, execute: work)
+ return
+ }
+
+ let lang = parent.language
+ let scheme = parent.colorScheme
+ let text = currentText ?? textView?.string ?? ""
+ if text == lastHighlightedText && lastLanguage == lang && lastColorScheme == scheme {
+ return
+ }
+ rehighlight()
+ }
+
+ func rehighlight() {
+ guard let textView = textView else { return }
+ // Snapshot current state
+ let textSnapshot = textView.string
+ let language = parent.language
+ let scheme = parent.colorScheme
+ let selected = textView.selectedRange()
+ let colors = SyntaxColors.fromVibrantLightTheme(colorScheme: scheme)
+ let patterns = getSyntaxPatterns(for: language, colors: colors)
+
+ // Cancel any in-flight work
+ pendingHighlight?.cancel()
+
+ let work = DispatchWorkItem { [weak self] in
+ // Compute matches off the main thread
+ let nsText = textSnapshot as NSString
+ let fullRange = NSRange(location: 0, length: nsText.length)
+ var coloredRanges: [(NSRange, Color)] = []
+ for (pattern, color) in patterns {
+ guard let regex = try? NSRegularExpression(pattern: pattern, options: [.anchorsMatchLines]) else { continue }
+ let matches = regex.matches(in: textSnapshot, range: fullRange)
+ for match in matches {
+ coloredRanges.append((match.range, color))
+ }
+ }
+
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self, let tv = self.textView else { return }
+ // Discard if text changed since we started
+ guard tv.string == textSnapshot else { return }
+
+ tv.textStorage?.beginEditing()
+ // Clear previous coloring and apply base color
+ tv.textStorage?.removeAttribute(.foregroundColor, range: fullRange)
+ tv.textStorage?.addAttribute(.foregroundColor, value: tv.textColor ?? NSColor.labelColor, range: fullRange)
+ // Apply colored ranges
+ for (range, color) in coloredRanges {
+ tv.textStorage?.addAttribute(.foregroundColor, value: NSColor(color), range: range)
+ }
+ tv.textStorage?.endEditing()
+
+ // Restore selection only if it hasn't changed since we started
+ if NSEqualRanges(tv.selectedRange(), selected) {
+ tv.setSelectedRange(selected)
+ }
+
+ // Update last highlighted state
+ self.lastHighlightedText = textSnapshot
+ self.lastLanguage = language
+ self.lastColorScheme = scheme
+ }
+ }
+
+ pendingHighlight = work
+ // Debounce slightly to avoid thrashing while typing
+ highlightQueue.asyncAfter(deadline: .now() + 0.12, execute: work)
+ }
+
+ func textDidChange(_ notification: Notification) {
+ guard let textView = notification.object as? NSTextView else { return }
+ parent.text = textView.string
+ updateCaretStatusAndHighlight()
+ scheduleHighlightIfNeeded(currentText: parent.text)
+ }
+
+ func textViewDidChangeSelection(_ notification: Notification) {
+ updateCaretStatusAndHighlight()
+ }
+
+ private func updateCaretStatusAndHighlight() {
+ guard let tv = textView else { return }
+ let ns = tv.string as NSString
+ let sel = tv.selectedRange()
+ let location = sel.location
+ let prefix = ns.substring(to: min(location, ns.length))
+ let line = prefix.reduce(1) { $1 == "\n" ? $0 + 1 : $0 }
+ let col: Int = {
+ if let lastNL = prefix.lastIndex(of: "\n") {
+ return prefix.distance(from: lastNL, to: prefix.endIndex) - 1
+ } else {
+ return prefix.count
+ }
+ }()
+ NotificationCenter.default.post(name: .caretPositionDidChange, object: nil, userInfo: ["line": line, "column": col])
+
+ // Highlight current line
+ let lineRange = ns.lineRange(for: NSRange(location: location, length: 0))
+ let fullRange = NSRange(location: 0, length: ns.length)
+ tv.textStorage?.beginEditing()
+ tv.textStorage?.removeAttribute(.backgroundColor, range: fullRange)
+ tv.textStorage?.addAttribute(.backgroundColor, value: NSColor.selectedTextBackgroundColor.withAlphaComponent(0.12), range: lineRange)
+ tv.textStorage?.endEditing()
+ }
+
+ @objc func moveToLine(_ notification: Notification) {
+ guard let lineOneBased = notification.object as? Int,
+ let textView = textView else { return }
+
+ // If there's no text, nothing to do
+ let currentText = textView.string
+ guard !currentText.isEmpty else { return }
+
+ // Cancel any in-flight highlight to prevent it from restoring an old selection
+ pendingHighlight?.cancel()
+
+ // Work with NSString/UTF-16 indices to match NSTextView expectations
+ let ns = currentText as NSString
+ let totalLength = ns.length
+
+ // Clamp target line to available line count (1-based input)
+ let linesArray = currentText.components(separatedBy: .newlines)
+ let clampedLineIndex = max(1, min(lineOneBased, linesArray.count)) - 1 // 0-based index
+
+ // Compute the UTF-16 location by summing UTF-16 lengths of preceding lines + newline characters
+ var location = 0
+ if clampedLineIndex > 0 {
+ for i in 0..<(clampedLineIndex) {
+ let lineNSString = linesArray[i] as NSString
+ location += lineNSString.length
+ // Add one for the newline that separates lines, as components(separatedBy:) drops separators
+ location += 1
+ }
+ }
+ // Safety clamp
+ location = max(0, min(location, totalLength))
+
+ // Move caret and scroll into view on the main thread
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self, let tv = self.textView else { return }
+ tv.window?.makeFirstResponder(tv)
+ // Ensure layout is up-to-date before scrolling
+ if let container = tv.textContainer {
+ tv.layoutManager?.ensureLayout(for: container)
+ }
+ tv.setSelectedRange(NSRange(location: location, length: 0))
+ tv.scrollRangeToVisible(NSRange(location: location, length: 0))
+
+ // Stronger highlight for the entire target line
+ let lineRange = ns.lineRange(for: NSRange(location: location, length: 0))
+ let fullRange = NSRange(location: 0, length: totalLength)
+ tv.textStorage?.beginEditing()
+ tv.textStorage?.removeAttribute(.backgroundColor, range: fullRange)
+ tv.textStorage?.addAttribute(.backgroundColor, value: NSColor.selectedTextBackgroundColor.withAlphaComponent(0.18), range: lineRange)
+ tv.textStorage?.endEditing()
+ }
+ }
+
@objc func streamSuggestion(_ notification: Notification) {
guard let stream = notification.object as? AsyncStream,
let textView = textView else { return }
-
+
Task {
for await chunk in stream {
textView.textStorage?.append(NSAttributedString(string: chunk))
@@ -594,51 +631,50 @@ struct CustomTextEditor: NSViewRepresentable {
}
}
}
-
- @objc func updateTextContainerSize() {
- if let tv = textView, let sv = tv.enclosingScrollView {
- tv.textContainer?.containerSize = NSSize(width: sv.contentSize.width, height: .greatestFiniteMagnitude)
- tv.textContainer?.widthTracksTextView = true
- }
+ }
+}
+
+final class LineNumberRulerView: NSRulerView {
+ private weak var textView: NSTextView?
+ private let font = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular)
+ private let textColor = NSColor.secondaryLabelColor
+ private let inset: CGFloat = 4
+
+ init(textView: NSTextView) {
+ self.textView = textView
+ super.init(scrollView: textView.enclosingScrollView, orientation: .verticalRuler)
+ self.clientView = textView
+ self.ruleThickness = 44
+ NotificationCenter.default.addObserver(self, selector: #selector(redraw), name: NSText.didChangeNotification, object: textView)
+ NotificationCenter.default.addObserver(self, selector: #selector(redraw), name: NSView.boundsDidChangeNotification, object: scrollView?.contentView)
+ }
+
+ required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
+
+ @objc private func redraw() { needsDisplay = true }
+
+ override func drawHashMarksAndLabels(in rect: NSRect) {
+ guard let tv = textView, let lm = tv.layoutManager, let tc = tv.textContainer else { return }
+ let ctx = NSString(string: tv.string)
+ let visibleGlyphRange = lm.glyphRange(forBoundingRect: tv.visibleRect, in: tc)
+ var lineNumber = 1
+ if visibleGlyphRange.location > 0 {
+ let charIndex = lm.characterIndexForGlyph(at: visibleGlyphRange.location)
+ let prefix = ctx.substring(to: charIndex)
+ lineNumber = prefix.reduce(1) { $1 == "\n" ? $0 + 1 : $0 }
}
-
- @objc func scrollViewBoundsDidChange(_ notification: Notification) {
- if let tv = textView, let sv = tv.enclosingScrollView {
- tv.textContainer?.containerSize = NSSize(width: sv.contentSize.width, height: .greatestFiniteMagnitude)
- tv.textContainer?.widthTracksTextView = true
- tv.invalidateIntrinsicContentSize()
- tv.layoutManager?.ensureLayout(for: tv.textContainer!)
- }
- }
-
- func textDidChange(_ notification: Notification) {
- guard let textView = notification.object as? NSTextView else { return }
- parent.text = textView.string
-
- if let container = textView.textContainer, let scrollView = textView.enclosingScrollView {
- container.containerSize = NSSize(width: scrollView.contentSize.width, height: .greatestFiniteMagnitude)
- container.widthTracksTextView = true
- textView.invalidateIntrinsicContentSize()
- textView.layoutManager?.ensureLayout(for: container)
- }
-
- applySyntaxHighlighting()
- }
-
- func applySyntaxHighlighting() {
- guard let textView = textView else { return }
- let fullRange = NSRange(location: 0, length: (textView.string as NSString).length)
- // Replace the line below with adaptive label color instead of removing attribute
- textView.textStorage?.addAttribute(.foregroundColor, value: NSColor.labelColor, range: fullRange)
- let colors = SyntaxColors.fromVibrantLightTheme(colorScheme: parent.colorScheme)
- let patterns = getSyntaxPatterns(for: parent.language, colors: colors)
- for (pattern, color) in patterns {
- guard let regex = try? NSRegularExpression(pattern: pattern, options: [.anchorsMatchLines]) else { continue }
- let matches = regex.matches(in: textView.string, range: fullRange)
- for match in matches {
- textView.textStorage?.addAttribute(.foregroundColor, value: NSColor(color), range: match.range)
- }
- }
+ var glyphIndex = visibleGlyphRange.location
+ while glyphIndex < visibleGlyphRange.upperBound {
+ var effectiveRange = NSRange(location: 0, length: 0)
+ let lineRect = lm.lineFragmentRect(forGlyphAt: glyphIndex, effectiveRange: &effectiveRange, withoutAdditionalLayout: true)
+ let y = (lineRect.minY - tv.visibleRect.origin.y) + 2 - tv.textContainerInset.height
+ let numberString = "\(lineNumber)" as NSString
+ let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: textColor]
+ let size = numberString.size(withAttributes: attributes)
+ let drawPoint = NSPoint(x: bounds.maxX - size.width - inset, y: y)
+ numberString.draw(at: drawPoint, withAttributes: attributes)
+ glyphIndex = effectiveRange.upperBound
+ lineNumber += 1
}
}
}
@@ -657,7 +693,7 @@ struct SyntaxColors {
let atom: Color
let builtin: Color
let type: Color
-
+
static func fromVibrantLightTheme(colorScheme: ColorScheme) -> SyntaxColors {
let baseColors: [String: (light: Color, dark: Color)] = [
"keyword": (light: Color(red: 251/255, green: 0/255, blue: 186/255), dark: Color(red: 251/255, green: 0/255, blue: 186/255)),
@@ -674,7 +710,7 @@ struct SyntaxColors {
"builtin": (light: Color(red: 255/255, green: 130/255, blue: 0/255), dark: Color(red: 255/255, green: 130/255, blue: 0/255)),
"type": (light: Color(red: 170/255, green: 0/255, blue: 160/255), dark: Color(red: 170/255, green: 0/255, blue: 160/255))
]
-
+
return SyntaxColors(
keyword: colorScheme == .dark ? baseColors["keyword"]!.dark : baseColors["keyword"]!.light,
string: colorScheme == .dark ? baseColors["string"]!.dark : baseColors["string"]!.light,
@@ -697,14 +733,55 @@ func getSyntaxPatterns(for language: String, colors: SyntaxColors) -> [String: C
switch language {
case "swift":
return [
- "\\b(func|struct|class|enum|protocol|extension|if|else|for|while|switch|case|default|guard|defer|throw|try|catch|return|init|deinit)\\b": colors.keyword,
+ // Keywords (extended to include `import`)
+ "\\b(func|struct|class|enum|protocol|extension|if|else|for|while|switch|case|default|guard|defer|throw|try|catch|return|init|deinit|import)\\b": colors.keyword,
+
+ // Strings and Characters
"\"[^\"]*\"": colors.string,
+ "'[^'\\](?:\\.[^'\\])*'": colors.string,
+
+ // Numbers
"\\b([0-9]+(\\.[0-9]+)?)\\b": colors.number,
+
+ // Comments (single and multi-line)
"//.*": colors.comment,
"/\\*([^*]|(\\*+[^*/]))*\\*+/": colors.comment,
+
+ // Documentation markup (triple slash and doc blocks)
+ "(?m)^(///).*$": colors.comment,
+ "/\\*\\*([\\s\\S]*?)\\*+/": colors.comment,
+ // Documentation keywords inside docs (e.g., - Parameter:, - Returns:)
+ "(?m)\\-\\s*(Parameter|Parameters|Returns|Throws|Note|Warning|See\\salso)\\s*:": colors.meta,
+
+ // Marks / TODO / FIXME
+ "(?m)//\\s*(MARK|TODO|FIXME)\\s*:.*$": colors.meta,
+
+ // URLs
+ "https?://[A-Za-z0-9._~:/?#@!$&'()*+,;=%-]+": colors.atom,
+ "file://[A-Za-z0-9._~:/?#@!$&'()*+,;=%-]+": colors.atom,
+
+ // Preprocessor statements (conditionals and directives)
+ "(?m)^#(if|elseif|else|endif|warning|error|available)\\b.*$": colors.keyword,
+
+ // Attributes like @available, @MainActor, etc.
"@\\w+": colors.attribute,
+
+ // Variable declarations
"\\b(var|let)\\b": colors.variable,
- "\\b(String|Int|Double|Bool)\\b": colors.type
+
+ // Common Swift types
+ "\\b(String|Int|Double|Bool)\\b": colors.type,
+
+ // Regex literals and components (Swift /…/)
+ "/[^/\\n]*/": colors.builtin, // whole regex literal
+ "\\(\\?<([A-Za-z_][A-Za-z0-9_]*)>": colors.def, // named capture start (?
+ "\\[[^\\]]*\\]": colors.property, // character classes
+ "[|*+?]": colors.meta, // regex operators
+
+ // Common SwiftUI property names like `body`
+ "\\bbody\\b": colors.property,
+ // Project-specific identifier you mentioned: `viewModel`
+ "\\bviewModel\\b": colors.property
]
case "python":
return [
@@ -755,203 +832,6 @@ func getSyntaxPatterns(for language: String, colors: SyntaxColors) -> [String: C
extension Notification.Name {
static let moveCursorToLine = Notification.Name("moveCursorToLine")
static let streamSuggestion = Notification.Name("streamSuggestion")
+ static let caretPositionDidChange = Notification.Name("caretPositionDidChange")
}
-=======
- }
-}
-
-struct HighlightedTextEditor: View {
- @Binding var text: String
- let language: String
- let colorScheme: ColorScheme
-
- var body: some View {
- TextEditor(text: $text)
- .font(.custom("SF Mono", size: 13))
- .padding(10)
- .background(.ultraThinMaterial)
- .overlay(
- GeometryReader { geo in
- HighlightOverlay(text: text, language: language, colorScheme: colorScheme)
- .frame(width: geo.size.width, height: geo.size.height)
- }
- )
- }
-}
-
-struct HighlightOverlay: View {
- let text: String
- let language: String
- let colorScheme: ColorScheme
-
- var body: some View {
- Text(text)
- .font(.custom("SF Mono", size: 13))
- .foregroundColor(.clear)
- .overlay(
- GeometryReader { geo in
- ZStack(alignment: .topLeading) {
- ForEach(highlightedRanges, id: \.id) { range in
- Text(range.text)
- .font(.custom("SF Mono", size: 13))
- .foregroundColor(range.color)
- .offset(range.offset)
- }
- }
- }
- )
- }
-
- private var highlightedRanges: [HighlightedRange] {
- var ranges: [HighlightedRange] = []
- let colors = SyntaxColors.fromVibrantLightTheme(colorScheme: colorScheme)
- let patterns = getSyntaxPatterns(for: language, colors: colors)
-
- for (pattern, color) in patterns {
- if let regex = try? NSRegularExpression(pattern: pattern, options: []) {
- let nsString = text as NSString
- let matches = regex.matches(in: text, range: NSRange(location: 0, length: nsString.length))
-
- for match in matches {
- let range = match.range
- let matchedText = nsString.substring(with: range)
- let lines = text[.. 0 ? 1 : 0)
- let xOffset = CGFloat(nsString.substring(with: NSRange(location: lineStart, length: range.location - lineStart)).width(usingFont: NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)))
- let yOffset = CGFloat(lineNumber) * 20 // Approximate line height
-
- ranges.append(HighlightedRange(
- id: UUID(),
- text: matchedText,
- color: Color(color),
- offset: CGSize(width: xOffset, height: yOffset)
- ))
- }
- }
- }
- return ranges
- }
-}
-
-struct HighlightedRange: Identifiable {
- let id: UUID
- let text: String
- let color: Color
- let offset: CGSize
-}
-
-struct SyntaxColors {
- let keyword: Color
- let string: Color
- let number: Color
- let comment: Color
- let attribute: Color
- let variable: Color
- let def: Color
- let property: Color
- let meta: Color
- let tag: Color
- let atom: Color
- let builtin: Color
- let type: Color
-
- static func fromVibrantLightTheme(colorScheme: ColorScheme) -> SyntaxColors {
- let baseColors: [String: (light: Color, dark: Color)] = [
- "keyword": (light: Color(red: 251/255, green: 0/255, blue: 186/255), dark: Color(red: 251/255, green: 0/255, blue: 186/255)),
- "string": (light: Color(red: 190/255, green: 0/255, blue: 255/255), dark: Color(red: 190/255, green: 0/255, blue: 255/255)),
- "number": (light: Color(red: 28/255, green: 0/255, blue: 207/255), dark: Color(red: 28/255, green: 0/255, blue: 207/255)),
- "comment": (light: Color(red: 93/255, green: 108/255, blue: 121/255), dark: Color(red: 150/255, green: 160/255, blue: 170/255)),
- "attribute": (light: Color(red: 57/255, green: 0/255, blue: 255/255), dark: Color(red: 57/255, green: 0/255, blue: 255/255)),
- "variable": (light: Color(red: 19/255, green: 0/255, blue: 255/255), dark: Color(red: 19/255, green: 0/255, blue: 255/255)),
- "def": (light: Color(red: 29/255, green: 196/255, blue: 83/255), dark: Color(red: 29/255, green: 196/255, blue: 83/255)),
- "property": (light: Color(red: 29/255, green: 196/255, blue: 83/255), dark: Color(red: 29/255, green: 0/255, blue: 160/255)),
- "meta": (light: Color(red: 255/255, green: 16/255, blue: 0/255), dark: Color(red: 255/255, green: 16/255, blue: 0/255)),
- "tag": (light: Color(red: 170/255, green: 0/255, blue: 160/255), dark: Color(red: 170/255, green: 0/255, blue: 160/255)),
- "atom": (light: Color(red: 28/255, green: 0/255, blue: 207/255), dark: Color(red: 28/255, green: 0/255, blue: 207/255)),
- "builtin": (light: Color(red: 255/255, green: 130/255, blue: 0/255), dark: Color(red: 255/255, green: 130/255, blue: 0/255)),
- "type": (light: Color(red: 170/255, green: 0/255, blue: 160/255), dark: Color(red: 170/255, green: 0/255, blue: 160/255))
- ]
-
- return SyntaxColors(
- keyword: colorScheme == .dark ? baseColors["keyword"]!.dark : baseColors["keyword"]!.light,
- string: colorScheme == .dark ? baseColors["string"]!.dark : baseColors["string"]!.light,
- number: colorScheme == .dark ? baseColors["number"]!.dark : baseColors["number"]!.light,
- comment: colorScheme == .dark ? baseColors["comment"]!.dark : baseColors["comment"]!.light,
- attribute: colorScheme == .dark ? baseColors["attribute"]!.dark : baseColors["attribute"]!.light,
- variable: colorScheme == .dark ? baseColors["variable"]!.dark : baseColors["variable"]!.light,
- def: colorScheme == .dark ? baseColors["def"]!.dark : baseColors["def"]!.light,
- property: colorScheme == .dark ? baseColors["property"]!.dark : baseColors["property"]!.light,
- meta: colorScheme == .dark ? baseColors["meta"]!.dark : baseColors["meta"]!.light,
- tag: colorScheme == .dark ? baseColors["tag"]!.dark : baseColors["tag"]!.light,
- atom: colorScheme == .dark ? baseColors["atom"]!.dark : baseColors["atom"]!.light,
- builtin: colorScheme == .dark ? baseColors["builtin"]!.dark : baseColors["builtin"]!.light,
- type: colorScheme == .dark ? baseColors["type"]!.dark : baseColors["type"]!.light
- )
- }
-}
-
-func getSyntaxPatterns(for language: String, colors: SyntaxColors) -> [String: Color] {
- switch language {
- case "swift":
- return [
- "\\b(func|struct|class|enum|protocol|extension|if|else|for|while|switch|case|default|guard|defer|throw|try|catch|return|init|deinit)\\b": colors.keyword,
- "\"[^\"]*\"": colors.string,
- "\\b([0-9]+(\\.[0-9]+)?)\\b": colors.number,
- "//.*": colors.comment,
- "/\\*([^*]|(\\*+[^*/]))*\\*+/": colors.comment,
- "@\\w+": colors.attribute,
- "\\b(var|let)\\b": colors.variable,
- "\\b(String|Int|Double|Bool)\\b": colors.type
- ]
- case "python":
- return [
- "\\b(def|class|if|else|for|while|try|except|with|as|import|from)\\b": colors.keyword,
- "\\b(int|str|float|bool|list|dict)\\b": colors.type,
- "\"[^\"]*\"|'[^']*'": colors.string,
- "\\b([0-9]+(\\.[0-9]+)?)\\b": colors.number,
- "#.*": colors.comment
- ]
- case "javascript":
- return [
- "\\b(function|var|let|const|if|else|for|while|do|try|catch)\\b": colors.keyword,
- "\\b(Number|String|Boolean|Object|Array)\\b": colors.type,
- "\"[^\"]*\"|'[^']*'|\\`[^\\`]*\\`": colors.string,
- "\\b([0-9]+(\\.[0-9]+)?)\\b": colors.number,
- "//.*|/\\*([^*]|(\\*+[^*/]))*\\*+/": colors.comment
- ]
- case "html":
- return ["<[^>]+>": colors.tag]
- case "css":
- return ["\\b([a-zA-Z-]+\\s*:\\s*[^;]+;)": colors.property]
- case "c", "cpp":
- return [
- "\\b(int|float|double|char|void|if|else|for|while|do|switch|case|return)\\b": colors.keyword,
- "\\b(int|float|double|char)\\b": colors.type,
- "\"[^\"]*\"": colors.string,
- "\\b([0-9]+(\\.[0-9]+)?)\\b": colors.number,
- "//.*|/\\*([^*]|(\\*+[^*/]))*\\*+/": colors.comment
- ]
- case "json":
- return [
- "\"[^\"]+\"\\s*:": colors.property,
- "\"[^\"]*\"": colors.string,
- "\\b([0-9]+(\\.[0-9]+)?)\\b": colors.number,
- "\\b(true|false|null)\\b": colors.keyword
- ]
- case "markdown":
- return [
- "^#+\\s*[^#]+": colors.keyword,
- "\\*\\*[^\\*\\*]+\\*\\*": colors.def,
- "\\_[^\\_]+\\_": colors.def
- ]
- default:
- return [:]
- }
-}
-
-extension Notification.Name {
- static let moveCursorToLine = Notification.Name("moveCursorToLine")
-}
->>>>>>> main
diff --git a/Neon Vision Editor/EditorViewModel.swift b/Neon Vision Editor/EditorViewModel.swift
index c15a411..724ab1a 100644
--- a/Neon Vision Editor/EditorViewModel.swift
+++ b/Neon Vision Editor/EditorViewModel.swift
@@ -18,6 +18,7 @@ class EditorViewModel: ObservableObject {
@Published var isBrainDumpMode: Bool = false
@Published var showingRename: Bool = false
@Published var renameText: String = ""
+ @Published var isLineWrapEnabled: Bool = true
var selectedTab: TabData? {
get { tabs.first(where: { $0.id == selectedTabID }) }
@@ -65,7 +66,6 @@ class EditorViewModel: ObservableObject {
}
}
-<<<<<<< HEAD
func closeTab(tab: TabData) {
tabs.removeAll { $0.id == tab.id }
if tabs.isEmpty {
@@ -75,8 +75,6 @@ class EditorViewModel: ObservableObject {
}
}
-=======
->>>>>>> main
func saveFile(tab: TabData) {
guard let index = tabs.firstIndex(where: { $0.id == tab.id }) else { return }
if let url = tabs[index].fileURL {
@@ -95,16 +93,14 @@ class EditorViewModel: ObservableObject {
let panel = NSSavePanel()
panel.nameFieldStringValue = tabs[index].name
panel.allowedContentTypes = [.text, .swiftSource, .pythonScript, .javaScript, .html, .css, .cSource, .json, UTType(importedAs: "public.markdown")]
-
- Task {
- if panel.runModal() == .OK, let url = panel.url {
- do {
- try tabs[index].content.write(to: url, atomically: true, encoding: .utf8)
- tabs[index].fileURL = url
- tabs[index].name = url.lastPathComponent
- } catch {
- print("Error saving file: \(error)")
- }
+
+ if panel.runModal() == .OK, let url = panel.url {
+ do {
+ try tabs[index].content.write(to: url, atomically: true, encoding: .utf8)
+ tabs[index].fileURL = url
+ tabs[index].name = url.lastPathComponent
+ } catch {
+ print("Error saving file: \(error)")
}
}
}
@@ -114,20 +110,18 @@ class EditorViewModel: ObservableObject {
panel.allowedContentTypes = [.text, .sourceCode, .swiftSource, .pythonScript, .javaScript, .html, .css, .cSource, .json, UTType(importedAs: "public.markdown")]
panel.allowsMultipleSelection = false
panel.canChooseDirectories = false
-
- Task {
- if panel.runModal() == .OK, let url = panel.url {
- do {
- let content = try String(contentsOf: url, encoding: .utf8)
- let newTab = TabData(name: url.lastPathComponent,
- content: content,
- language: languageMap[url.pathExtension.lowercased()] ?? "swift",
- fileURL: url)
- tabs.append(newTab)
- selectedTabID = newTab.id
- } catch {
- print("Error opening file: \(error)")
- }
+
+ if panel.runModal() == .OK, let url = panel.url {
+ do {
+ let content = try String(contentsOf: url, encoding: .utf8)
+ let newTab = TabData(name: url.lastPathComponent,
+ content: content,
+ language: languageMap[url.pathExtension.lowercased()] ?? "swift",
+ fileURL: url)
+ tabs.append(newTab)
+ selectedTabID = newTab.id
+ } catch {
+ print("Error opening file: \(error)")
}
}
}
diff --git a/Neon Vision Editor/NeonVisionEditorApp.swift b/Neon Vision Editor/NeonVisionEditorApp.swift
index 56a0859..9bda359 100644
--- a/Neon Vision Editor/NeonVisionEditorApp.swift
+++ b/Neon Vision Editor/NeonVisionEditorApp.swift
@@ -1,46 +1,25 @@
import SwiftUI
-<<<<<<< HEAD
import FoundationModels
-enum AIModel: String, Identifiable {
- case appleIntelligence = "Apple Intelligence"
- case grok = "Grok"
-
- var id: String { rawValue }
-}
-=======
->>>>>>> main
-
@main
struct NeonVisionEditorApp: App {
@StateObject private var viewModel = EditorViewModel()
-<<<<<<< HEAD
@State private var showGrokError: Bool = false
@State private var grokErrorMessage: String = ""
- @State private var selectedAIModel: AIModel = .appleIntelligence
-=======
->>>>>>> main
+ @State private var useAppleIntelligence: Bool = true
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
-<<<<<<< HEAD
.environment(\.showGrokError, $showGrokError)
.environment(\.grokErrorMessage, $grokErrorMessage)
- .environment(\.selectedAIModel, $selectedAIModel)
.frame(minWidth: 600, minHeight: 400)
- .background(.ultraThinMaterial)
- .overlay(.ultraThinMaterial.opacity(0.2)) // Fallback for liquidGlassEffect
.task {
// Pre-warm Apple Intelligence model
let session = LanguageModelSession(model: SystemLanguageModel())
session.prewarm()
}
-=======
- .frame(minWidth: 600, minHeight: 400)
- .background(.ultraThinMaterial)
->>>>>>> main
}
.defaultSize(width: 1000, height: 600)
.commands {
@@ -75,7 +54,6 @@ struct NeonVisionEditorApp: App {
viewModel.renameText = viewModel.selectedTab?.name ?? "Untitled"
}
.disabled(viewModel.selectedTab == nil)
-<<<<<<< HEAD
Button("Close Tab") {
if let tab = viewModel.selectedTab {
@@ -84,8 +62,6 @@ struct NeonVisionEditorApp: App {
}
.keyboardShortcut("w", modifiers: .command)
.disabled(viewModel.selectedTab == nil)
-=======
->>>>>>> main
}
CommandMenu("Language") {
@@ -105,15 +81,16 @@ struct NeonVisionEditorApp: App {
Toggle("Brain Dump Mode", isOn: $viewModel.isBrainDumpMode)
.keyboardShortcut("d", modifiers: [.command, .shift])
+
+ Toggle("Line Wrap", isOn: $viewModel.isLineWrapEnabled)
+ .keyboardShortcut("l", modifiers: [.command, .option])
}
CommandMenu("Tools") {
-<<<<<<< HEAD
Button("Suggest Code") {
Task {
if let tab = viewModel.selectedTab {
- switch selectedAIModel {
- case .appleIntelligence:
+ if useAppleIntelligence {
let session = LanguageModelSession(model: SystemLanguageModel())
let prompt = "System: Output a code suggestion for this \(tab.language) code.\nUser: \(tab.content.prefix(1000))"
do {
@@ -123,7 +100,7 @@ struct NeonVisionEditorApp: App {
grokErrorMessage = error.localizedDescription
showGrokError = true
}
- case .grok:
+ } else {
let client = GrokAPIClient(apiKey: "your-xai-api-key") // Replace with your xAI API key from https://x.ai/api
let prompt = "Suggest improvements for this \(tab.language) code: \(tab.content.prefix(1000))"
do {
@@ -133,26 +110,14 @@ struct NeonVisionEditorApp: App {
grokErrorMessage = error.localizedDescription
showGrokError = true
}
-=======
- Button("Suggest Code with Grok") {
- Task {
- let client = GrokAPIClient(apiKey: "your-xai-api-key") // Replace with your actual xAI API key from https://x.ai/api
- if let tab = viewModel.selectedTab {
- let prompt = "Suggest improvements for this \(tab.language) code: \(tab.content.prefix(1000))"
- do {
- let suggestion = try await client.generateText(prompt: prompt, maxTokens: 200)
- // Append suggestion to the current content
- viewModel.updateTabContent(tab: tab, content: tab.content + "\n\n// Grok Suggestion:\n" + suggestion)
- } catch {
- print("Grok API error: \(error)")
- // Optional: Show an alert or sheet for the error
->>>>>>> main
}
}
}
}
.keyboardShortcut("g", modifiers: [.command, .shift])
.disabled(viewModel.selectedTab == nil)
+
+ Toggle("Use Apple Intelligence", isOn: $useAppleIntelligence)
}
}
}
@@ -166,10 +131,6 @@ struct GrokErrorMessageKey: EnvironmentKey {
static let defaultValue: Binding = .constant("")
}
-struct SelectedAIModelKey: EnvironmentKey {
- static let defaultValue: Binding = .constant(.appleIntelligence)
-}
-
extension EnvironmentValues {
var showGrokError: Binding {
get { self[ShowGrokErrorKey.self] }
@@ -180,9 +141,4 @@ extension EnvironmentValues {
get { self[GrokErrorMessageKey.self] }
set { self[GrokErrorMessageKey.self] = newValue }
}
-
- var selectedAIModel: Binding {
- get { self[SelectedAIModelKey.self] }
- set { self[SelectedAIModelKey.self] = newValue }
- }
}