app-adjustments for textfield

This commit is contained in:
Rodric Krogh 2026-01-15 19:02:41 +01:00
commit 881fcf6332
4 changed files with 459 additions and 0 deletions

View file

@ -1,5 +1,6 @@
// FIXES APPLIED: Consistent rename and content persistence for tab creation and language updates
import SwiftUI
<<<<<<< HEAD
import AppKit
// Extension to calculate string width
@ -135,9 +136,149 @@ struct ContentView: View {
)
.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 {
@ -224,10 +365,32 @@ struct SidebarView: View {
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)
@ -240,20 +403,36 @@ 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("<h")) {
return "\(trimmed) (Line \(index + 1))"
@ -261,7 +440,11 @@ struct SidebarView: View {
return nil
}
default:
<<<<<<< HEAD
return ["Unsupported language"]
=======
return []
>>>>>>> main
}
return toc.isEmpty ? ["No headers found"] : toc
@ -270,6 +453,7 @@ struct SidebarView: View {
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
}
}
@ -573,3 +757,201 @@ extension Notification.Name {
static let streamSuggestion = Notification.Name("streamSuggestion")
}
=======
}
}
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[..<nsString.substring(to: range.location).endIndex].components(separatedBy: .newlines)
let lineNumber = lines.count - 1
let lineStart = lines.dropLast().joined(separator: "\n").count + (lineNumber > 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

View file

@ -65,6 +65,7 @@ class EditorViewModel: ObservableObject {
}
}
<<<<<<< HEAD
func closeTab(tab: TabData) {
tabs.removeAll { $0.id == tab.id }
if tabs.isEmpty {
@ -74,6 +75,8 @@ 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 {

View file

@ -0,0 +1,45 @@
import Foundation
class GrokAPIClient {
private let apiKey: String
private let baseURL = "https://api.x.ai/v1"
init(apiKey: String) {
self.apiKey = apiKey
}
func generateText(prompt: String, model: String = "grok-3-beta", maxTokens: Int = 500) async throws -> String {
let url = URL(string: "\(baseURL)/chat/completions")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"model": model,
"messages": [
["role": "user", "content": prompt]
],
"max_tokens": maxTokens
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw NSError(domain: "GrokAPI", code: -1, userInfo: [NSLocalizedDescriptionKey: "API request failed"])
}
let json = try JSONDecoder().decode(GrokResponse.self, from: data)
return json.choices.first?.message.content ?? ""
}
}
struct GrokResponse: Codable {
struct Choice: Codable {
struct Message: Codable {
let content: String
}
let message: Message
}
let choices: [Choice]
}

View file

@ -1,4 +1,5 @@
import SwiftUI
<<<<<<< HEAD
import FoundationModels
enum AIModel: String, Identifiable {
@ -7,18 +8,24 @@ enum AIModel: String, Identifiable {
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
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
<<<<<<< HEAD
.environment(\.showGrokError, $showGrokError)
.environment(\.grokErrorMessage, $grokErrorMessage)
.environment(\.selectedAIModel, $selectedAIModel)
@ -30,6 +37,10 @@ struct NeonVisionEditorApp: App {
let session = LanguageModelSession(model: SystemLanguageModel())
session.prewarm()
}
=======
.frame(minWidth: 600, minHeight: 400)
.background(.ultraThinMaterial)
>>>>>>> main
}
.defaultSize(width: 1000, height: 600)
.commands {
@ -64,6 +75,7 @@ struct NeonVisionEditorApp: App {
viewModel.renameText = viewModel.selectedTab?.name ?? "Untitled"
}
.disabled(viewModel.selectedTab == nil)
<<<<<<< HEAD
Button("Close Tab") {
if let tab = viewModel.selectedTab {
@ -72,6 +84,8 @@ struct NeonVisionEditorApp: App {
}
.keyboardShortcut("w", modifiers: .command)
.disabled(viewModel.selectedTab == nil)
=======
>>>>>>> main
}
CommandMenu("Language") {
@ -94,6 +108,7 @@ struct NeonVisionEditorApp: App {
}
CommandMenu("Tools") {
<<<<<<< HEAD
Button("Suggest Code") {
Task {
if let tab = viewModel.selectedTab {
@ -118,6 +133,20 @@ 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
}
}
}