2025-09-25 09:00:22 +00:00
|
|
|
import SwiftUI
|
2025-09-25 09:01:45 +00:00
|
|
|
import Combine
|
2025-09-25 09:00:22 +00:00
|
|
|
import UniformTypeIdentifiers
|
|
|
|
|
|
2025-09-25 09:01:45 +00:00
|
|
|
struct TabData: Identifiable {
|
|
|
|
|
let id = UUID()
|
|
|
|
|
var name: String
|
|
|
|
|
var content: String
|
|
|
|
|
var language: String
|
|
|
|
|
var fileURL: URL?
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@MainActor
|
2025-09-25 09:00:22 +00:00
|
|
|
class EditorViewModel: ObservableObject {
|
2025-09-25 09:01:45 +00:00
|
|
|
@Published var tabs: [TabData] = []
|
|
|
|
|
@Published var selectedTabID: UUID?
|
2025-09-25 09:00:22 +00:00
|
|
|
@Published var showSidebar: Bool = true
|
2025-09-25 09:01:45 +00:00
|
|
|
@Published var isBrainDumpMode: Bool = false
|
|
|
|
|
@Published var showingRename: Bool = false
|
|
|
|
|
@Published var renameText: String = ""
|
2026-01-17 11:11:26 +00:00
|
|
|
@Published var isLineWrapEnabled: Bool = true
|
2025-09-25 09:01:45 +00:00
|
|
|
|
|
|
|
|
var selectedTab: TabData? {
|
|
|
|
|
get { tabs.first(where: { $0.id == selectedTabID }) }
|
|
|
|
|
set { selectedTabID = newValue?.id }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private let languageMap: [String: String] = [
|
|
|
|
|
"swift": "swift",
|
|
|
|
|
"py": "python",
|
|
|
|
|
"js": "javascript",
|
|
|
|
|
"html": "html",
|
|
|
|
|
"css": "css",
|
|
|
|
|
"c": "c",
|
|
|
|
|
"cpp": "cpp",
|
|
|
|
|
"h": "c",
|
|
|
|
|
"json": "json",
|
2026-01-23 11:49:52 +00:00
|
|
|
"md": "markdown",
|
|
|
|
|
"sh": "bash",
|
|
|
|
|
"bash": "bash",
|
|
|
|
|
"zsh": "zsh"
|
2025-09-25 09:01:45 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
init() {
|
|
|
|
|
addNewTab()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func addNewTab() {
|
|
|
|
|
let newTab = TabData(name: "Untitled \(tabs.count + 1)", content: "", language: "swift", fileURL: nil)
|
2025-09-25 09:00:22 +00:00
|
|
|
tabs.append(newTab)
|
2025-09-25 09:01:45 +00:00
|
|
|
selectedTabID = newTab.id
|
2025-09-25 09:00:22 +00:00
|
|
|
}
|
2025-09-25 09:01:45 +00:00
|
|
|
|
|
|
|
|
func renameTab(tab: TabData, newName: String) {
|
|
|
|
|
if let index = tabs.firstIndex(where: { $0.id == tab.id }) {
|
|
|
|
|
tabs[index].name = newName
|
2025-09-25 09:00:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-09-25 09:01:45 +00:00
|
|
|
|
|
|
|
|
func updateTabContent(tab: TabData, content: String) {
|
|
|
|
|
if let index = tabs.firstIndex(where: { $0.id == tab.id }) {
|
|
|
|
|
tabs[index].content = content
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateTabLanguage(tab: TabData, language: String) {
|
|
|
|
|
if let index = tabs.firstIndex(where: { $0.id == tab.id }) {
|
|
|
|
|
tabs[index].language = language
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func closeTab(tab: TabData) {
|
|
|
|
|
tabs.removeAll { $0.id == tab.id }
|
|
|
|
|
if tabs.isEmpty {
|
|
|
|
|
addNewTab()
|
|
|
|
|
} else if selectedTabID == tab.id {
|
|
|
|
|
selectedTabID = tabs.first?.id
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func saveFile(tab: TabData) {
|
|
|
|
|
guard let index = tabs.firstIndex(where: { $0.id == tab.id }) else { return }
|
|
|
|
|
if let url = tabs[index].fileURL {
|
2025-09-25 09:00:22 +00:00
|
|
|
do {
|
2025-09-25 09:01:45 +00:00
|
|
|
try tabs[index].content.write(to: url, atomically: true, encoding: .utf8)
|
2025-09-25 09:00:22 +00:00
|
|
|
} catch {
|
|
|
|
|
print("Error saving file: \(error)")
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
saveFileAs(tab: tab)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-25 09:01:45 +00:00
|
|
|
|
|
|
|
|
func saveFileAs(tab: TabData) {
|
|
|
|
|
guard let index = tabs.firstIndex(where: { $0.id == tab.id }) else { return }
|
|
|
|
|
let panel = NSSavePanel()
|
|
|
|
|
panel.nameFieldStringValue = tabs[index].name
|
|
|
|
|
panel.allowedContentTypes = [.text, .swiftSource, .pythonScript, .javaScript, .html, .css, .cSource, .json, UTType(importedAs: "public.markdown")]
|
2026-01-17 11:11:26 +00:00
|
|
|
|
|
|
|
|
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)")
|
2025-09-25 09:00:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-25 09:01:45 +00:00
|
|
|
|
|
|
|
|
func openFile() {
|
|
|
|
|
let panel = NSOpenPanel()
|
2026-01-23 11:49:52 +00:00
|
|
|
// Allow opening any file type, including hidden dotfiles like .zshrc
|
|
|
|
|
panel.allowedContentTypes = []
|
|
|
|
|
panel.allowsOtherFileTypes = true
|
2025-09-25 09:01:45 +00:00
|
|
|
panel.allowsMultipleSelection = false
|
|
|
|
|
panel.canChooseDirectories = false
|
2026-01-23 11:49:52 +00:00
|
|
|
panel.showsHiddenFiles = true
|
2026-01-17 11:11:26 +00:00
|
|
|
|
|
|
|
|
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)")
|
2025-09-25 09:01:45 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 12:46:33 +00:00
|
|
|
func openFile(url: 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)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-25 09:01:45 +00:00
|
|
|
func wordCount(for text: String) -> Int {
|
|
|
|
|
text.components(separatedBy: .whitespacesAndNewlines)
|
|
|
|
|
.filter { !$0.isEmpty }.count
|
|
|
|
|
}
|
|
|
|
|
}
|