Improve JSON editor for macOS

This commit is contained in:
世界
2025-11-26 22:25:52 +08:00
parent 3f1c716a0a
commit c5fd6a6b4e
25 changed files with 979 additions and 322 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "Frameworks/Runestone"]
path = Frameworks/Runestone
url = https://github.com/nekohasekai/Runestone.git
+1
View File
@@ -0,0 +1 @@
--exclude Frameworks
@@ -11,20 +11,65 @@ import SwiftUI
} }
public var body: some View { public var body: some View {
NavigationStackCompat { #if os(macOS)
Group { macOSBody
if configuration.isLoading { #else
ProgressView() iOSBody
} else { #endif
listContent }
#if os(macOS)
private var macOSBody: some View {
VStack(alignment: .leading, spacing: 0) {
Text("Dashboard Items")
.font(.headline)
.padding(.horizontal, 20)
.padding(.top, 20)
.padding(.bottom, 12)
Group {
if configuration.isLoading {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
listContent
}
} }
} }
.navigationTitle("Dashboard Items") .toolbar {
#if os(iOS) ToolbarItem(placement: .destructiveAction) {
.navigationBarTitleDisplayMode(.inline) Button("Reset", role: .destructive) {
#endif Task {
.toolbar { await configuration.resetToDefault()
#if os(iOS) || os(tvOS) configurationVersion += 1
}
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
dismiss()
}
}
}
.onExitCommand {
dismiss()
}
}
#else
private var iOSBody: some View {
NavigationStackCompat {
Group {
if configuration.isLoading {
ProgressView()
} else {
listContent
}
}
.navigationTitle("Dashboard Items")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
Button("Reset", role: .destructive) { Button("Reset", role: .destructive) {
Task { Task {
@@ -33,24 +78,10 @@ import SwiftUI
} }
} }
} }
#else }
ToolbarItem(placement: .cancellationAction) { }
Button("Reset", role: .destructive) {
Task {
await configuration.resetToDefault()
configurationVersion += 1
}
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
dismiss()
}
}
#endif
}
} }
} #endif
private var listContent: some View { private var listContent: some View {
List { List {
@@ -216,7 +216,7 @@ public struct ProfileCard: View {
private var manageProfilesSheet: some View { private var manageProfilesSheet: some View {
NavigationSheet( NavigationSheet(
title: "Profiles", title: String(localized: "Manage profiles"),
showDoneButton: true, showDoneButton: true,
onDismiss: { viewModel.showManageProfiles = false } onDismiss: { viewModel.showManageProfiles = false }
) { ) {
@@ -227,11 +227,20 @@ public struct ProfileCard: View {
@ViewBuilder @ViewBuilder
private func editProfileSheet(for profile: Profile) -> some View { private func editProfileSheet(for profile: Profile) -> some View {
NavigationSheet(title: "Edit Profile") { #if os(macOS)
EditProfileView() NavigationSheet {
.environmentObject(profile) EditProfileView()
.environmentObject(environments) .environmentObject(profile)
} .environmentObject(environments)
}
.frame(minWidth: 500, minHeight: 400)
#else
NavigationSheet(title: "Edit Profile") {
EditProfileView()
.environmentObject(profile)
.environmentObject(environments)
}
#endif
} }
} }
@@ -272,7 +281,16 @@ extension ProfileCard {
@State private var createdProfile: Profile? @State private var createdProfile: Profile?
var body: some View { var body: some View {
NavigationStackCompat { #if os(macOS)
macOSBody
#else
iOSBody
#endif
}
#if os(macOS)
@ViewBuilder
private var macOSBody: some View {
if let profile = createdProfile { if let profile = createdProfile {
EditProfileView() EditProfileView()
.environmentObject(profile) .environmentObject(profile)
@@ -282,15 +300,28 @@ extension ProfileCard {
createdProfile = profile createdProfile = profile
} }
.environmentObject(environments) .environmentObject(environments)
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
} }
} }
#if os(iOS) || os(tvOS) #else
.presentationDetentsIfAvailable() private var iOSBody: some View {
#endif NavigationStackCompat {
} if let profile = createdProfile {
EditProfileView()
.environmentObject(profile)
.environmentObject(environments)
} else {
NewProfileView { profile in
createdProfile = profile
}
.environmentObject(environments)
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
}
.presentationDetentsIfAvailable()
}
#endif
} }
} }
@@ -305,12 +336,14 @@ extension ProfileCard {
var body: some View { var body: some View {
VStack { VStack {
if viewModel.isLoading { if viewModel.isLoading {
ProgressView().onAppear { ProgressView()
viewModel.setEnvironments(environments) .frame(maxWidth: .infinity, maxHeight: .infinity)
Task { .onAppear {
await viewModel.doReload() viewModel.setEnvironments(environments)
Task {
await viewModel.doReload()
}
} }
}
} else { } else {
FormView { FormView {
if viewModel.profileList.isEmpty { if viewModel.profileList.isEmpty {
@@ -19,6 +19,7 @@
} }
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@Environment(\.profileEditor) private var profileEditor
public var body: some View { public var body: some View {
viewBuilder { viewBuilder {
@@ -29,30 +30,10 @@
} }
} }
} else { } else {
#if os(iOS) editorView
RunestoneTextView(
text: readOnly ? .constant(viewModel.profileContent) : $viewModel.profileContent,
isEditable: !readOnly
)
.onChangeCompat(of: viewModel.profileContent) { .onChangeCompat(of: viewModel.profileContent) {
viewModel.markAsChanged() viewModel.markAsChanged()
} }
#elseif os(macOS)
viewBuilder {
if readOnly {
TextEditor(text: .constant(viewModel.profileContent))
} else {
TextEditor(text: $viewModel.profileContent)
}
}
.font(Font.system(.caption2, design: .monospaced))
.autocorrectionDisabled(true)
.textContentType(.init(rawValue: ""))
.padding()
.onChangeCompat(of: viewModel.profileContent) {
viewModel.markAsChanged()
}
#endif
} }
} }
.alertBinding($viewModel.alert) .alertBinding($viewModel.alert)
@@ -105,6 +86,38 @@
return String(localized: "Edit Content") return String(localized: "Edit Content")
} }
} }
@ViewBuilder
private var editorView: some View {
if let profileEditor {
profileEditor(
readOnly ? .constant(viewModel.profileContent) : $viewModel.profileContent,
!readOnly
)
#if os(macOS)
.frame(maxWidth: .infinity, maxHeight: .infinity)
#endif
} else {
defaultEditorView
}
}
@ViewBuilder
private var defaultEditorView: some View {
viewBuilder {
if readOnly {
TextEditor(text: .constant(viewModel.profileContent))
} else {
TextEditor(text: $viewModel.profileContent)
}
}
.font(Font.system(.caption2, design: .monospaced))
.autocorrectionDisabled(true)
#if os(macOS)
.textContentType(.init(rawValue: ""))
.padding()
#endif
}
} }
#endif #endif
@@ -21,6 +21,14 @@
isChanged = true isChanged = true
} }
public func reset() {
isLoading = true
profile = nil
profileContent = ""
isChanged = false
alert = nil
}
public func loadContent() async { public func loadContent() async {
do { do {
try await loadContentBackground() try await loadContentBackground()
@@ -10,7 +10,16 @@ public struct EditProfileView: View {
@StateObject private var viewModel = EditProfileViewModel() @StateObject private var viewModel = EditProfileViewModel()
public init() {} public init() {}
public var body: some View { public var body: some View {
#if os(macOS)
macOSBody
#else
iOSBody
#endif
}
private var formContent: some View {
FormView { FormView {
FormItem(String(localized: "Name")) { FormItem(String(localized: "Name")) {
TextField("Name", text: $profile.name, prompt: Text("Required")) TextField("Name", text: $profile.name, prompt: Text("Required"))
@@ -60,48 +69,61 @@ public struct EditProfileView: View {
ProfileActionToolbar(profile: profile, viewModel: viewModel) ProfileActionToolbar(profile: profile, viewModel: viewModel)
#endif #endif
} }
#if os(macOS)
.safeAreaInset(edge: .bottom) {
ProfileActionToolbar(profile: profile, viewModel: viewModel)
}
#endif
.onChangeCompat(of: profile.name) {
viewModel.markAsChanged()
}
.onChangeCompat(of: profile.remoteURL) {
viewModel.markAsChanged()
}
.onChangeCompat(of: profile.autoUpdate) {
viewModel.markAsChanged()
}
.disabled(viewModel.isLoading)
#if os(macOS)
.toolbar {
ToolbarItemGroup(placement: .navigation) {
Button {
viewModel.isLoading = true
Task {
await viewModel.saveProfile(profile, environments: environments)
}
} label: {
Image("save", bundle: ApplicationLibrary.bundle, label: Text("Save"))
}
.disabled(viewModel.isLoading || !viewModel.isChanged)
}
}
#elseif os(iOS)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
viewModel.isLoading = true
Task {
await viewModel.saveProfile(profile, environments: environments)
}
}.disabled(!viewModel.isChanged)
}
}
#endif
.alertBinding($viewModel.alert)
.navigationTitle("Edit Profile")
} }
#if os(macOS)
private var macOSBody: some View {
VStack(alignment: .leading, spacing: 0) {
Text("Edit Profile")
.font(.headline)
.padding(.horizontal, 20)
.padding(.top, 20)
.padding(.bottom, 12)
formContent
}
.safeAreaInset(edge: .bottom) {
ProfileActionToolbar(profile: profile, viewModel: viewModel)
}
.onChangeCompat(of: profile.name) {
viewModel.markAsChanged()
}
.onChangeCompat(of: profile.remoteURL) {
viewModel.markAsChanged()
}
.onChangeCompat(of: profile.autoUpdate) {
viewModel.markAsChanged()
}
.disabled(viewModel.isLoading)
.alertBinding($viewModel.alert)
}
#else
private var iOSBody: some View {
formContent
.onChangeCompat(of: profile.name) {
viewModel.markAsChanged()
}
.onChangeCompat(of: profile.remoteURL) {
viewModel.markAsChanged()
}
.onChangeCompat(of: profile.autoUpdate) {
viewModel.markAsChanged()
}
.disabled(viewModel.isLoading)
#if os(iOS)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
viewModel.isLoading = true
Task {
await viewModel.saveProfile(profile, environments: environments)
}
}.disabled(!viewModel.isChanged)
}
}
#endif
.alertBinding($viewModel.alert)
.navigationTitle("Edit Profile")
}
#endif
} }
@@ -25,6 +25,14 @@ public struct NewProfileView: View {
} }
public var body: some View { public var body: some View {
#if os(macOS)
macOSBody
#else
iOSBody
#endif
}
private var formContent: some View {
FormView { FormView {
FormItem(String(localized: "Name")) { FormItem(String(localized: "Name")) {
TextField("Name", text: $viewModel.profileName, prompt: Text("Required")) TextField("Name", text: $viewModel.profileName, prompt: Text("Required"))
@@ -113,8 +121,19 @@ public struct NewProfileView: View {
} }
#endif #endif
} }
.navigationTitle("New Profile") }
#if os(macOS)
#if os(macOS)
private var macOSBody: some View {
VStack(alignment: .leading, spacing: 0) {
Text("New Profile")
.font(.headline)
.padding(.horizontal, 20)
.padding(.top, 20)
.padding(.bottom, 12)
formContent
}
.toolbar { .toolbar {
ToolbarItem(placement: .cancellationAction) { ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { Button("Cancel") {
@@ -138,10 +157,8 @@ public struct NewProfileView: View {
} }
} }
} }
#endif
.disabled(viewModel.isSaving) .disabled(viewModel.isSaving)
.alertBinding($viewModel.alert) .alertBinding($viewModel.alert)
#if os(iOS) || os(macOS)
.fileImporter( .fileImporter(
isPresented: $viewModel.pickerPresented, isPresented: $viewModel.pickerPresented,
allowedContentTypes: [.json], allowedContentTypes: [.json],
@@ -157,6 +174,30 @@ public struct NewProfileView: View {
return return
} }
} }
#endif }
} #else
private var iOSBody: some View {
formContent
.navigationTitle("New Profile")
.disabled(viewModel.isSaving)
.alertBinding($viewModel.alert)
#if os(iOS)
.fileImporter(
isPresented: $viewModel.pickerPresented,
allowedContentTypes: [.json],
allowsMultipleSelection: false
) { result in
do {
let urls = try result.get()
if !urls.isEmpty {
viewModel.fileURL = urls[0]
}
} catch {
viewModel.alert = Alert(error)
return
}
}
#endif
}
#endif
} }
@@ -6,6 +6,9 @@ import SwiftUI
public struct ProfileActionToolbar: View { public struct ProfileActionToolbar: View {
@EnvironmentObject private var environments: ExtensionEnvironments @EnvironmentObject private var environments: ExtensionEnvironments
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
#if os(macOS)
@Environment(\.openWindow) private var openWindow
#endif
@ObservedObject private var profile: Profile @ObservedObject private var profile: Profile
@ObservedObject private var viewModel: EditProfileViewModel @ObservedObject private var viewModel: EditProfileViewModel
@@ -69,16 +72,12 @@ public struct ProfileActionToolbar: View {
HStack(spacing: 12) { HStack(spacing: 12) {
if profile.type != .remote { if profile.type != .remote {
NavigationLink { Button("Edit Content") {
EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: false)) openWindow(value: EditProfileContentView.Context(profileID: profile.id!, readOnly: false))
} label: {
Text("Edit Content")
} }
} else { } else {
NavigationLink { Button("View Content") {
EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: true)) openWindow(value: EditProfileContentView.Context(profileID: profile.id!, readOnly: true))
} label: {
Text("View Content")
} }
Button { Button {
@@ -92,14 +91,27 @@ public struct ProfileActionToolbar: View {
.disabled(viewModel.isLoading) .disabled(viewModel.isLoading)
} }
Spacer()
Button("Delete", role: .destructive) { Button("Delete", role: .destructive) {
Task { Task {
await viewModel.deleteProfile(profile, environments: environments, dismiss: dismiss) await viewModel.deleteProfile(profile, environments: environments, dismiss: dismiss)
} }
} }
.foregroundColor(.red) .foregroundColor(.red)
Spacer()
Button("Cancel") {
dismiss()
}
Button("Save") {
viewModel.isLoading = true
Task {
await viewModel.saveProfile(profile, environments: environments)
}
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.isLoading || !viewModel.isChanged)
} }
.padding() .padding()
.background(Color(NSColor.controlBackgroundColor)) .background(Color(NSColor.controlBackgroundColor))
@@ -0,0 +1,14 @@
#if os(iOS) || os(macOS)
import SwiftUI
public struct ProfileEditorKey: EnvironmentKey {
public static let defaultValue: ((Binding<String>, Bool) -> AnyView)? = nil
}
public extension EnvironmentValues {
var profileEditor: ((Binding<String>, Bool) -> AnyView)? {
get { self[ProfileEditorKey.self] }
set { self[ProfileEditorKey.self] = newValue }
}
}
#endif
@@ -1,73 +0,0 @@
#if os(iOS)
import Runestone
import UIKit
final class ProfileEditorTheme: Theme {
let font: UIFont = .monospacedSystemFont(ofSize: 14, weight: .regular)
let textColor: UIColor = .label
let gutterBackgroundColor: UIColor = .secondarySystemBackground
let gutterHairlineColor: UIColor = .separator
let lineNumberColor: UIColor = .secondaryLabel
let lineNumberFont: UIFont = .monospacedSystemFont(ofSize: 14, weight: .regular)
let selectedLineBackgroundColor: UIColor = .systemFill
let selectedLinesLineNumberColor: UIColor = .label
let selectedLinesGutterBackgroundColor: UIColor = .secondarySystemBackground
let invisibleCharactersColor: UIColor = .tertiaryLabel
let pageGuideHairlineColor: UIColor = .separator
let pageGuideBackgroundColor: UIColor = .secondarySystemBackground
let markedTextBackgroundColor: UIColor = .systemFill
let markedTextBackgroundCornerRadius: CGFloat = 4
func textColor(for rawHighlightName: String) -> UIColor? {
guard let highlightName = HighlightName(rawHighlightName) else {
return nil
}
switch highlightName {
case .comment:
return .secondaryLabel
case .property:
return .systemCyan
case .string:
return .systemGreen
case .number:
return .systemOrange
case .constantBuiltin:
return .systemPurple
case .error:
return .systemRed
}
}
func fontTraits(for _: String) -> FontTraits {
[]
}
}
private enum HighlightName: String {
case comment
case property
case string
case number
case constantBuiltin = "constant.builtin"
case error
init?(_ rawHighlightName: String) {
var components = rawHighlightName.split(separator: ".")
while !components.isEmpty {
let candidateRawHighlightName = components.joined(separator: ".")
if let highlightName = Self(rawValue: candidateRawHighlightName) {
self = highlightName
return
}
components.removeLast()
}
return nil
}
}
#endif
@@ -18,14 +18,14 @@ public enum SheetSize {
@MainActor @MainActor
public struct NavigationSheet<Content: View>: View { public struct NavigationSheet<Content: View>: View {
private let title: String private let title: String?
private let size: SheetSize private let size: SheetSize
private let showDoneButton: Bool private let showDoneButton: Bool
private let onDismiss: (() -> Void)? private let onDismiss: (() -> Void)?
private let content: () -> Content private let content: () -> Content
public init( public init(
title: String, title: String? = nil,
size: SheetSize = .large, size: SheetSize = .large,
showDoneButton: Bool = false, showDoneButton: Bool = false,
onDismiss: (() -> Void)? = nil, onDismiss: (() -> Void)? = nil,
@@ -39,13 +39,25 @@ public struct NavigationSheet<Content: View>: View {
} }
public var body: some View { public var body: some View {
NavigationStackCompat { #if os(macOS)
content() macOSBody
.navigationTitle(title) #else
#if os(iOS) iOSBody
.navigationBarTitleDisplayMode(.inline) #endif
#endif }
#if os(macOS)
#if os(macOS)
private var macOSBody: some View {
VStack(alignment: .leading, spacing: 0) {
if let title {
Text(title)
.font(.headline)
.padding(.horizontal, 20)
.padding(.top, 20)
.padding(.bottom, 12)
}
content()
}
.toolbar { .toolbar {
if showDoneButton { if showDoneButton {
ToolbarItem(placement: .confirmationAction) { ToolbarItem(placement: .confirmationAction) {
@@ -55,12 +67,19 @@ public struct NavigationSheet<Content: View>: View {
} }
} }
} }
#endif
} }
#if os(iOS) || os(tvOS) #else
.sheetDetent(size) private var iOSBody: some View {
#endif NavigationStackCompat {
} content()
.navigationTitle(title ?? "")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
.sheetDetent(size)
}
#endif
} }
#if os(iOS) || os(tvOS) #if os(iOS) || os(tvOS)
@@ -1,85 +0,0 @@
#if os(iOS)
import Runestone
import SwiftUI
import TreeSitterJSON5Runestone
struct RunestoneTextView: UIViewRepresentable {
@Binding var text: String
let isEditable: Bool
func makeUIView(context: Context) -> TextView {
let textView = TextView()
textView.showLineNumbers = true
textView.isLineWrappingEnabled = false
textView.showTabs = false
textView.showSpaces = false
textView.showLineBreaks = false
textView.showSoftLineBreaks = false
textView.showNonBreakingSpaces = false
textView.autocorrectionType = .no
textView.autocapitalizationType = .none
textView.smartDashesType = .no
textView.smartQuotesType = .no
textView.smartInsertDeleteType = .no
textView.backgroundColor = .secondarySystemGroupedBackground
textView.contentInsetAdjustmentBehavior = .always
textView.alwaysBounceVertical = true
textView.kern = 0.3
textView.lineHeightMultiplier = 1.3
textView.characterPairs = [
BasicCharacterPair(leading: "{", trailing: "}"),
BasicCharacterPair(leading: "[", trailing: "]"),
BasicCharacterPair(leading: "\"", trailing: "\""),
]
let theme = ProfileEditorTheme()
let state = TextViewState(text: text, theme: theme, language: .json5)
textView.setState(state)
textView.isEditable = isEditable
textView.editorDelegate = context.coordinator
return textView
}
func updateUIView(_ textView: TextView, context _: Context) {
if textView.text != text {
textView.text = text
}
if textView.isEditable != isEditable {
textView.isEditable = isEditable
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: TextViewDelegate {
var parent: RunestoneTextView
init(_ parent: RunestoneTextView) {
self.parent = parent
}
func textViewDidChange(_ textView: TextView) {
parent.text = textView.text
}
}
}
final class BasicCharacterPair: CharacterPair {
let leading: String
let trailing: String
init(leading: String, trailing: String) {
self.leading = leading
self.trailing = trailing
}
}
#endif
+1 -1
View File
@@ -13,7 +13,7 @@ let package = Package(
.library(name: "TreeSitterJSON5Runestone", targets: ["TreeSitterJSON5Runestone"]), .library(name: "TreeSitterJSON5Runestone", targets: ["TreeSitterJSON5Runestone"]),
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/simonbs/Runestone", from: "0.3.0"), .package(path: "../Runestone"),
], ],
targets: [ targets: [
.target(name: "TreeSitterJSON5", cSettings: [.headerSearchPath("src")]), .target(name: "TreeSitterJSON5", cSettings: [.headerSearchPath("src")]),
+16
View File
@@ -195,6 +195,9 @@
} }
} }
} }
},
"Cancel" : {
}, },
"Chain" : { "Chain" : {
"localizations" : { "localizations" : {
@@ -507,6 +510,9 @@
} }
} }
} }
},
"Do you want to save the changes you made?" : {
}, },
"Documentation" : { "Documentation" : {
"localizations" : { "localizations" : {
@@ -527,6 +533,9 @@
} }
} }
} }
},
"Don't Save" : {
}, },
"Done" : { "Done" : {
"localizations" : { "localizations" : {
@@ -1017,6 +1026,9 @@
}, },
"Machine: " : { "Machine: " : {
"shouldTranslate" : false "shouldTranslate" : false
},
"Manage profiles" : {
}, },
"Match Rule" : { "Match Rule" : {
"shouldTranslate" : false "shouldTranslate" : false
@@ -1295,6 +1307,7 @@
} }
}, },
"Profiles" : { "Profiles" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"zh-Hans" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
@@ -1746,6 +1759,9 @@
} }
} }
} }
},
"Unsaved Changes" : {
}, },
"Update" : { "Update" : {
"localizations" : { "localizations" : {
+127
View File
@@ -0,0 +1,127 @@
import AppKit
import CodeEditLanguages
import CodeEditSourceEditor
import CodeEditTextView
import SwiftUI
private extension NSColor {
var forEditor: NSColor {
usingColorSpace(.sRGB) ?? self
}
}
private func makeTheme() -> EditorTheme {
EditorTheme(
text: .init(color: NSColor.labelColor.forEditor),
insertionPoint: NSColor.labelColor.forEditor,
invisibles: .init(color: NSColor.tertiaryLabelColor.forEditor),
background: NSColor.textBackgroundColor.forEditor,
lineHighlight: NSColor.quaternaryLabelColor.forEditor,
selection: NSColor.selectedTextBackgroundColor.forEditor,
keywords: .init(color: NSColor.systemPurple.forEditor),
commands: .init(color: NSColor.systemCyan.forEditor),
types: .init(color: NSColor.systemCyan.forEditor),
attributes: .init(color: NSColor.systemCyan.forEditor),
variables: .init(color: NSColor.labelColor.forEditor),
values: .init(color: NSColor.systemOrange.forEditor),
numbers: .init(color: NSColor.systemOrange.forEditor),
strings: .init(color: NSColor.systemGreen.forEditor),
characters: .init(color: NSColor.systemGreen.forEditor),
comments: .init(color: NSColor.secondaryLabelColor.forEditor)
)
}
private func makeConfiguration(isEditable: Bool) -> SourceEditorConfiguration {
SourceEditorConfiguration(
appearance: .init(
theme: makeTheme(),
font: .monospacedSystemFont(ofSize: 14, weight: .regular),
lineHeightMultiple: 1.3,
wrapLines: false
),
behavior: .init(
isEditable: isEditable,
isSelectable: true
),
peripherals: .init(
showMinimap: false,
showFoldingRibbon: false
)
)
}
struct CodeEditTextView: NSViewRepresentable {
@Binding var text: String
let isEditable: Bool
func makeNSView(context: Context) -> NSView {
let controller = TextViewController(
string: text,
language: .json,
configuration: makeConfiguration(isEditable: isEditable),
cursorPositions: []
)
controller.loadView()
let containerView = NSView()
containerView.translatesAutoresizingMaskIntoConstraints = false
let controllerView = controller.view
controllerView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(controllerView)
NSLayoutConstraint.activate([
controllerView.topAnchor.constraint(equalTo: containerView.topAnchor),
controllerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
controllerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
controllerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
])
context.coordinator.controller = controller
context.coordinator.setupObservation()
return containerView
}
func updateNSView(_: NSView, context: Context) {
guard let controller = context.coordinator.controller else { return }
if controller.text != text {
controller.text = text
}
if controller.configuration.behavior.isEditable != isEditable {
controller.configuration = makeConfiguration(isEditable: isEditable)
}
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
class Coordinator: NSObject {
var controller: TextViewController?
@Binding var text: String
private var observation: NSObjectProtocol?
init(text: Binding<String>) {
_text = text
super.init()
}
func setupObservation() {
guard let controller else { return }
observation = NotificationCenter.default.addObserver(
forName: TextView.textDidChangeNotification,
object: controller.textView,
queue: .main
) { [weak self] _ in
guard let self, let controller = self.controller else { return }
self.text = controller.text
}
}
deinit {
if let observation {
NotificationCenter.default.removeObserver(observation)
}
}
}
}
+204
View File
@@ -0,0 +1,204 @@
import ApplicationLibrary
import Library
import SwiftUI
struct EditProfileContentWindow: View {
let context: EditProfileContentView.Context?
@StateObject private var viewModel: EditProfileContentViewModel
@State private var showDiscardAlert = false
@State private var windowState = WindowState()
private let readOnly: Bool
init(context: EditProfileContentView.Context?) {
self.context = context
readOnly = context?.readOnly == true
_viewModel = StateObject(wrappedValue: EditProfileContentViewModel(profileID: context?.profileID))
}
@Environment(\.profileEditor) private var profileEditor
var body: some View {
Group {
if viewModel.isLoading {
ProgressView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.task {
await viewModel.loadContent()
}
} else {
editorView
.onChangeCompat(of: viewModel.profileContent) {
viewModel.markAsChanged()
}
}
}
.frame(minWidth: 600, minHeight: 400)
.background(WindowAccessor { window in
guard let window else { return }
if windowState.window == nil {
windowState.window = window
windowState.onClose = { [weak viewModel] in
viewModel?.reset()
}
let delegate = WindowCloseDelegate(
windowState: windowState,
hasUnsavedChanges: { [weak viewModel] in
viewModel?.isChanged == true
},
showAlert: {
showDiscardAlert = true
}
)
windowState.delegate = delegate
window.delegate = delegate
}
})
.onExitCommand {
handleClose()
}
.alert("Unsaved Changes", isPresented: $showDiscardAlert) {
Button("Don't Save", role: .destructive) {
windowState.forceClose()
}
Button("Cancel", role: .cancel) {}
if !readOnly {
Button("Save") {
Task {
await viewModel.saveContent()
if viewModel.alert == nil {
windowState.forceClose()
}
}
}
}
} message: {
Text("Do you want to save the changes you made?")
}
.alertBinding($viewModel.alert)
.navigationTitle(navigationTitle)
.toolbar {
ToolbarItemGroup(placement: .navigation) {
if !readOnly {
Button {
Task {
await viewModel.saveContent()
}
} label: {
Label("Save", image: "save")
}
.keyboardShortcut("s", modifiers: .command)
.disabled(!viewModel.isChanged)
} else {
Button {
NSPasteboard.general.setString(viewModel.profileContent, forType: .string)
} label: {
Label("Copy", systemImage: "doc.on.clipboard")
}
}
}
}
}
private var navigationTitle: String {
if readOnly {
return String(localized: "View Content")
} else {
return String(localized: "Edit Content")
}
}
@ViewBuilder
private var editorView: some View {
if let profileEditor {
profileEditor(
readOnly ? .constant(viewModel.profileContent) : $viewModel.profileContent,
!readOnly
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
defaultEditorView
}
}
@ViewBuilder
private var defaultEditorView: some View {
Group {
if readOnly {
TextEditor(text: .constant(viewModel.profileContent))
} else {
TextEditor(text: $viewModel.profileContent)
}
}
.font(Font.system(.caption2, design: .monospaced))
.autocorrectionDisabled(true)
.textContentType(.init(rawValue: ""))
.padding()
}
private func handleClose() {
if viewModel.isChanged, !readOnly {
showDiscardAlert = true
} else {
windowState.forceClose()
}
}
}
private class WindowState {
weak var window: NSWindow?
var delegate: WindowCloseDelegate?
var onClose: (() -> Void)?
func forceClose() {
delegate?.allowClose = true
window?.close()
}
}
private struct WindowAccessor: NSViewRepresentable {
let callback: (NSWindow?) -> Void
func makeNSView(context _: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
callback(view.window)
}
return view
}
func updateNSView(_: NSView, context _: Context) {}
}
private class WindowCloseDelegate: NSObject, NSWindowDelegate {
var allowClose = false
private let windowState: WindowState
private let hasUnsavedChanges: () -> Bool
private let showAlert: () -> Void
init(windowState: WindowState, hasUnsavedChanges: @escaping () -> Bool, showAlert: @escaping () -> Void) {
self.windowState = windowState
self.hasUnsavedChanges = hasUnsavedChanges
self.showAlert = showAlert
super.init()
}
func windowShouldClose(_: NSWindow) -> Bool {
if allowClose {
return true
}
if hasUnsavedChanges() {
DispatchQueue.main.async {
self.showAlert()
}
return false
}
return true
}
func windowWillClose(_: Notification) {
windowState.onClose?()
allowClose = false
}
}
+11
View File
@@ -7,6 +7,10 @@ public struct MacApplication: Scene {
@State private var isMenuPresented = false @State private var isMenuPresented = false
@StateObject private var environments = ExtensionEnvironments() @StateObject private var environments = ExtensionEnvironments()
private let profileEditor: (Binding<String>, Bool) -> AnyView = { text, isEditable in
AnyView(CodeEditTextView(text: text, isEditable: isEditable))
}
public init() {} public init() {}
public var body: some Scene { public var body: some Scene {
Window("sing-box", id: "main", content: { Window("sing-box", id: "main", content: {
@@ -52,6 +56,13 @@ public struct MacApplication: Scene {
} }
.menuBarExtraStyle(.window) .menuBarExtraStyle(.window)
.menuBarExtraAccess(isPresented: $isMenuPresented) .menuBarExtraAccess(isPresented: $isMenuPresented)
WindowGroup(for: EditProfileContentView.Context.self) { $context in
EditProfileContentWindow(context: context)
.environment(\.profileEditor, profileEditor)
}
.windowResizability(.contentMinSize)
.defaultSize(width: 700, height: 500)
} }
private func initialize() async { private func initialize() async {
+5
View File
@@ -10,6 +10,10 @@ public struct MainView: View {
@State private var showCardManagement = false @State private var showCardManagement = false
@State private var cardConfigurationVersion = 0 @State private var cardConfigurationVersion = 0
private let profileEditor: (Binding<String>, Bool) -> AnyView = { text, isEditable in
AnyView(CodeEditTextView(text: text, isEditable: isEditable))
}
public init() {} public init() {}
public var body: some View { public var body: some View {
@@ -59,6 +63,7 @@ public struct MainView: View {
.environment(\.selection, $viewModel.selection) .environment(\.selection, $viewModel.selection)
.environment(\.importProfile, $viewModel.importProfile) .environment(\.importProfile, $viewModel.importProfile)
.environment(\.importRemoteProfile, $viewModel.importRemoteProfile) .environment(\.importRemoteProfile, $viewModel.importRemoteProfile)
.environment(\.profileEditor, profileEditor)
.handlesExternalEvents(preferring: [], allowing: ["*"]) .handlesExternalEvents(preferring: [], allowing: ["*"])
.onOpenURL(perform: viewModel.openURL) .onOpenURL(perform: viewModel.openURL)
.sheet(isPresented: $showCardManagement) { .sheet(isPresented: $showCardManagement) {
+6
View File
@@ -16,6 +16,10 @@ struct MainView: View {
@State private var showConnections = false @State private var showConnections = false
@State private var buttonState = ButtonVisibilityState() @State private var buttonState = ButtonVisibilityState()
private let profileEditor: (Binding<String>, Bool) -> AnyView = { text, isEditable in
AnyView(RunestoneTextView(text: text, isEditable: isEditable))
}
private var shouldShowBottomAccessory: Bool { private var shouldShowBottomAccessory: Bool {
guard !environments.extensionProfileLoading else { guard !environments.extensionProfileLoading else {
return false return false
@@ -124,6 +128,7 @@ struct MainView: View {
.environment(\.selection, $selection) .environment(\.selection, $selection)
.environment(\.importProfile, $importProfile) .environment(\.importProfile, $importProfile)
.environment(\.importRemoteProfile, $importRemoteProfile) .environment(\.importRemoteProfile, $importRemoteProfile)
.environment(\.profileEditor, profileEditor)
.handlesExternalEvents(preferring: [], allowing: ["*"]) .handlesExternalEvents(preferring: [], allowing: ["*"])
.onOpenURL(perform: openURL) .onOpenURL(perform: openURL)
.sheet(isPresented: $showGroups) { .sheet(isPresented: $showGroups) {
@@ -160,6 +165,7 @@ struct MainView: View {
.environment(\.selection, $selection) .environment(\.selection, $selection)
.environment(\.importProfile, $importProfile) .environment(\.importProfile, $importProfile)
.environment(\.importRemoteProfile, $importRemoteProfile) .environment(\.importRemoteProfile, $importRemoteProfile)
.environment(\.profileEditor, profileEditor)
.handlesExternalEvents(preferring: [], allowing: ["*"]) .handlesExternalEvents(preferring: [], allowing: ["*"])
.onOpenURL(perform: openURL) .onOpenURL(perform: openURL)
} }
+71
View File
@@ -0,0 +1,71 @@
import Runestone
import UIKit
final class ProfileEditorTheme: Theme {
let font: UIFont = .monospacedSystemFont(ofSize: 14, weight: .regular)
let textColor: UIColor = .label
let gutterBackgroundColor: UIColor = .secondarySystemBackground
let gutterHairlineColor: UIColor = .separator
let lineNumberColor: UIColor = .secondaryLabel
let lineNumberFont: UIFont = .monospacedSystemFont(ofSize: 14, weight: .regular)
let selectedLineBackgroundColor: UIColor = .systemFill
let selectedLinesLineNumberColor: UIColor = .label
let selectedLinesGutterBackgroundColor: UIColor = .secondarySystemBackground
let invisibleCharactersColor: UIColor = .tertiaryLabel
let pageGuideHairlineColor: UIColor = .separator
let pageGuideBackgroundColor: UIColor = .secondarySystemBackground
let markedTextBackgroundColor: UIColor = .systemFill
let markedTextBackgroundCornerRadius: CGFloat = 4
func textColor(for rawHighlightName: String) -> UIColor? {
guard let highlightName = HighlightName(rawHighlightName) else {
return nil
}
switch highlightName {
case .comment:
return .secondaryLabel
case .property:
return .systemCyan
case .string:
return .systemGreen
case .number:
return .systemOrange
case .constantBuiltin:
return .systemPurple
case .error:
return .systemRed
}
}
func fontTraits(for _: String) -> FontTraits {
[]
}
}
private enum HighlightName: String {
case comment
case property
case string
case number
case constantBuiltin = "constant.builtin"
case error
init?(_ rawHighlightName: String) {
var components = rawHighlightName.split(separator: ".")
while !components.isEmpty {
let candidateRawHighlightName = components.joined(separator: ".")
if let highlightName = Self(rawValue: candidateRawHighlightName) {
self = highlightName
return
}
components.removeLast()
}
return nil
}
}
+83
View File
@@ -0,0 +1,83 @@
import Runestone
import SwiftUI
import TreeSitterJSON5Runestone
struct RunestoneTextView: UIViewRepresentable {
@Binding var text: String
let isEditable: Bool
func makeUIView(context: Context) -> TextView {
let textView = TextView()
textView.showLineNumbers = true
textView.isLineWrappingEnabled = false
textView.showTabs = false
textView.showSpaces = false
textView.showLineBreaks = false
textView.showSoftLineBreaks = false
textView.showNonBreakingSpaces = false
textView.autocorrectionType = .no
textView.autocapitalizationType = .none
textView.smartDashesType = .no
textView.smartQuotesType = .no
textView.smartInsertDeleteType = .no
textView.backgroundColor = .secondarySystemGroupedBackground
textView.contentInsetAdjustmentBehavior = .always
textView.alwaysBounceVertical = true
textView.kern = 0.3
textView.lineHeightMultiplier = 1.3
textView.characterPairs = [
BasicCharacterPair(leading: "{", trailing: "}"),
BasicCharacterPair(leading: "[", trailing: "]"),
BasicCharacterPair(leading: "\"", trailing: "\""),
]
let theme = ProfileEditorTheme()
let state = TextViewState(text: text, theme: theme, language: .json5)
textView.setState(state)
textView.isEditable = isEditable
textView.editorDelegate = context.coordinator
return textView
}
func updateUIView(_ textView: TextView, context _: Context) {
if textView.text != text {
textView.text = text
}
if textView.isEditable != isEditable {
textView.isEditable = isEditable
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: TextViewDelegate {
var parent: RunestoneTextView
init(_ parent: RunestoneTextView) {
self.parent = parent
}
func textViewDidChange(_ textView: TextView) {
parent.text = textView.text
}
}
}
final class BasicCharacterPair: CharacterPair {
let leading: String
let trailing: String
init(leading: String, trailing: String) {
self.leading = leading
self.trailing = trailing
}
}
+27 -14
View File
@@ -29,6 +29,7 @@
3A9759202A4EB69C00E4404B /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; 3A9759202A4EB69C00E4404B /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
3A9759212A4EB69C00E4404B /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3A9759212A4EB69C00E4404B /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
3AC194492A50013F00BD8CB9 /* IntentsExtension.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 3AC194492A50013F00BD8CB9 /* IntentsExtension.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
3ACE5E032EE1A91300644196 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACE5E022EE1A91200644196 /* CodeEditSourceEditor */; };
3AE1719A2A8128DD00393060 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */; }; 3AE1719A2A8128DD00393060 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */; };
3AE171A62A81294400393060 /* TVExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3AE171992A8128DD00393060 /* TVExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 3AE171A62A81294400393060 /* TVExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3AE171992A8128DD00393060 /* TVExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
3AE171A92A81297300393060 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; 3AE171A92A81297300393060 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; };
@@ -536,8 +537,6 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
3A4EAD1B2A4FEB02005435B3 /* Library.framework in Frameworks */, 3A4EAD1B2A4FEB02005435B3 /* Library.framework in Frameworks */,
3A2E87FB2ED5ABDA00644195 /* TreeSitterJSON5Runestone in Frameworks */,
3A2E87F22ED5A91100644195 /* Runestone in Frameworks */,
3A4A020D2B53E3DC004EFB87 /* QRCode in Frameworks */, 3A4A020D2B53E3DC004EFB87 /* QRCode in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -585,6 +584,8 @@
files = ( files = (
3A9759202A4EB69C00E4404B /* Library.framework in Frameworks */, 3A9759202A4EB69C00E4404B /* Library.framework in Frameworks */,
3A4EAD372A4FEC20005435B3 /* ApplicationLibrary.framework in Frameworks */, 3A4EAD372A4FEC20005435B3 /* ApplicationLibrary.framework in Frameworks */,
3A2E87F22ED5A91100644195 /* Runestone in Frameworks */,
3A2E87FB2ED5ABDA00644195 /* TreeSitterJSON5Runestone in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -633,6 +634,7 @@
files = ( files = (
3AEECC452A6DFE61006A0E0C /* ApplicationLibrary.framework in Frameworks */, 3AEECC452A6DFE61006A0E0C /* ApplicationLibrary.framework in Frameworks */,
3AEECC4E2A6DFF13006A0E0C /* MacControlCenterUI in Frameworks */, 3AEECC4E2A6DFF13006A0E0C /* MacControlCenterUI in Frameworks */,
3ACE5E032EE1A91300644196 /* CodeEditSourceEditor in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -758,8 +760,6 @@
name = ApplicationLibrary; name = ApplicationLibrary;
packageProductDependencies = ( packageProductDependencies = (
3A4A020C2B53E3DC004EFB87 /* QRCode */, 3A4A020C2B53E3DC004EFB87 /* QRCode */,
3A2E87F12ED5A91100644195 /* Runestone */,
3A2E87FA2ED5ABDA00644195 /* TreeSitterJSON5Runestone */,
); );
productName = ApplicationLibrary; productName = ApplicationLibrary;
productReference = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */; productReference = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */;
@@ -872,6 +872,8 @@
); );
name = SFI; name = SFI;
packageProductDependencies = ( packageProductDependencies = (
3A2E87F12ED5A91100644195 /* Runestone */,
3A2E87FA2ED5ABDA00644195 /* TreeSitterJSON5Runestone */,
); );
productName = SFI; productName = SFI;
productReference = 3AEC20F32A459AB400A63465 /* sing-box.app */; productReference = 3AEC20F32A459AB400A63465 /* sing-box.app */;
@@ -992,6 +994,7 @@
name = MacLibrary; name = MacLibrary;
packageProductDependencies = ( packageProductDependencies = (
3AEECC4D2A6DFF13006A0E0C /* MacControlCenterUI */, 3AEECC4D2A6DFF13006A0E0C /* MacControlCenterUI */,
3ACE5E022EE1A91200644196 /* CodeEditSourceEditor */,
); );
productName = MacLibrary; productName = MacLibrary;
productReference = 3AEECC2F2A6DFDAD006A0E0C /* MacLibrary.framework */; productReference = 3AEECC2F2A6DFDAD006A0E0C /* MacLibrary.framework */;
@@ -1064,8 +1067,9 @@
3A017F902A4AB2E4009149FA /* XCRemoteSwiftPackageReference "GRDB" */, 3A017F902A4AB2E4009149FA /* XCRemoteSwiftPackageReference "GRDB" */,
3A57DF3A2A4D705000690BC5 /* XCRemoteSwiftPackageReference "MacControlCenterUI" */, 3A57DF3A2A4D705000690BC5 /* XCRemoteSwiftPackageReference "MacControlCenterUI" */,
3A4A020B2B53E3DC004EFB87 /* XCRemoteSwiftPackageReference "qrcode" */, 3A4A020B2B53E3DC004EFB87 /* XCRemoteSwiftPackageReference "qrcode" */,
3A2E87F02ED5A91100644195 /* XCRemoteSwiftPackageReference "Runestone" */, 3A2E87F02ED5A91100644195 /* XCLocalSwiftPackageReference "Frameworks/Runestone" */,
3A2E87F92ED5ABCF00644195 /* XCLocalSwiftPackageReference "Frameworks/TreeSitterJSON5" */, 3A2E87F92ED5ABCF00644195 /* XCLocalSwiftPackageReference "Frameworks/TreeSitterJSON5" */,
3ACE5E012EE1A91100644196 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */,
); );
productRefGroup = 3AEC20C72A45991900A63465 /* Products */; productRefGroup = 3AEC20C72A45991900A63465 /* Products */;
projectDirPath = ""; projectDirPath = "";
@@ -2562,6 +2566,10 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */ /* Begin XCLocalSwiftPackageReference section */
3A2E87F02ED5A91100644195 /* XCLocalSwiftPackageReference "Frameworks/Runestone" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Frameworks/Runestone;
};
3A2E87F92ED5ABCF00644195 /* XCLocalSwiftPackageReference "Frameworks/TreeSitterJSON5" */ = { 3A2E87F92ED5ABCF00644195 /* XCLocalSwiftPackageReference "Frameworks/TreeSitterJSON5" */ = {
isa = XCLocalSwiftPackageReference; isa = XCLocalSwiftPackageReference;
relativePath = Frameworks/TreeSitterJSON5; relativePath = Frameworks/TreeSitterJSON5;
@@ -2577,14 +2585,6 @@
minimumVersion = 6.15.1; minimumVersion = 6.15.1;
}; };
}; };
3A2E87F02ED5A91100644195 /* XCRemoteSwiftPackageReference "Runestone" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/simonbs/Runestone";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.5.1;
};
};
3A4A020B2B53E3DC004EFB87 /* XCRemoteSwiftPackageReference "qrcode" */ = { 3A4A020B2B53E3DC004EFB87 /* XCRemoteSwiftPackageReference "qrcode" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/dagronf/qrcode.git"; repositoryURL = "https://github.com/dagronf/qrcode.git";
@@ -2609,6 +2609,14 @@
minimumVersion = 2.0.0; minimumVersion = 2.0.0;
}; };
}; };
3ACE5E012EE1A91100644196 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.15.2;
};
};
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@@ -2619,7 +2627,7 @@
}; };
3A2E87F12ED5A91100644195 /* Runestone */ = { 3A2E87F12ED5A91100644195 /* Runestone */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 3A2E87F02ED5A91100644195 /* XCRemoteSwiftPackageReference "Runestone" */; package = 3A2E87F02ED5A91100644195 /* XCLocalSwiftPackageReference "Frameworks/Runestone" */;
productName = Runestone; productName = Runestone;
}; };
3A2E87FA2ED5ABDA00644195 /* TreeSitterJSON5Runestone */ = { 3A2E87FA2ED5ABDA00644195 /* TreeSitterJSON5Runestone */ = {
@@ -2637,6 +2645,11 @@
package = 3A7E90362A46778E00D53052 /* XCRemoteSwiftPackageReference "BinaryCodable" */; package = 3A7E90362A46778E00D53052 /* XCRemoteSwiftPackageReference "BinaryCodable" */;
productName = BinaryCodable; productName = BinaryCodable;
}; };
3ACE5E022EE1A91200644196 /* CodeEditSourceEditor */ = {
isa = XCSwiftPackageProductDependency;
package = 3ACE5E012EE1A91100644196 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */;
productName = CodeEditSourceEditor;
};
3AEECC4D2A6DFF13006A0E0C /* MacControlCenterUI */ = { 3AEECC4D2A6DFF13006A0E0C /* MacControlCenterUI */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 3A57DF3A2A4D705000690BC5 /* XCRemoteSwiftPackageReference "MacControlCenterUI" */; package = 3A57DF3A2A4D705000690BC5 /* XCRemoteSwiftPackageReference "MacControlCenterUI" */;
@@ -1,5 +1,5 @@
{ {
"originHash" : "48cbd083120736a1a1ce4b717dcbfdb7900aa5e2aa21991865021fb98adc2093", "originHash" : "3ae592e476adf467b19c2cde1d4ba44683fb6f05a27fc1959105b8eb14cce1d2",
"pins" : [ "pins" : [
{ {
"identity" : "binarycodable", "identity" : "binarycodable",
@@ -10,6 +10,42 @@
"version" : "2.0.3" "version" : "2.0.3"
} }
}, },
{
"identity" : "codeeditlanguages",
"kind" : "remoteSourceControl",
"location" : "https://github.com/CodeEditApp/CodeEditLanguages.git",
"state" : {
"revision" : "331d5dbc5fc8513be5848fce8a2a312908f36a11",
"version" : "0.1.20"
}
},
{
"identity" : "codeeditsourceeditor",
"kind" : "remoteSourceControl",
"location" : "https://github.com/CodeEditApp/CodeEditSourceEditor.git",
"state" : {
"revision" : "424453d2232c9912933a3b5a1f3d3df669404ed0",
"version" : "0.15.2"
}
},
{
"identity" : "codeeditsymbols",
"kind" : "remoteSourceControl",
"location" : "https://github.com/CodeEditApp/CodeEditSymbols.git",
"state" : {
"revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e",
"version" : "0.2.3"
}
},
{
"identity" : "codeedittextview",
"kind" : "remoteSourceControl",
"location" : "https://github.com/CodeEditApp/CodeEditTextView.git",
"state" : {
"revision" : "d7ac3f11f22ec2e820187acce8f3a3fb7aa8ddec",
"version" : "0.12.1"
}
},
{ {
"identity" : "grdb.swift", "identity" : "grdb.swift",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
@@ -47,12 +83,21 @@
} }
}, },
{ {
"identity" : "runestone", "identity" : "rearrange",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/simonbs/Runestone", "location" : "https://github.com/ChimeHQ/Rearrange",
"state" : { "state" : {
"revision" : "1fad339aab99cf2136ce6bf8c32da3265b2e85e5", "revision" : "f1d74e1642956f0300756ad8d1d64e9034857bc3",
"version" : "0.5.1" "version" : "2.0.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e",
"version" : "1.3.0"
} }
}, },
{ {
@@ -73,13 +118,49 @@
"version" : "1.6.1" "version" : "1.6.1"
} }
}, },
{
"identity" : "swiftlintplugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/lukepistrol/SwiftLintPlugin",
"state" : {
"revision" : "9bbc46a4cf8275ceb39334f6276b6b215d67d5d5",
"version" : "0.62.2"
}
},
{
"identity" : "swifttreesitter",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git",
"state" : {
"revision" : "08ef81eb8620617b55b08868126707ad72bf754f",
"version" : "0.25.0"
}
},
{
"identity" : "textformation",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/TextFormation",
"state" : {
"revision" : "b1ce9a14bd86042bba4de62236028dc4ce9db6a1",
"version" : "0.9.0"
}
},
{
"identity" : "textstory",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ChimeHQ/TextStory",
"state" : {
"revision" : "91df6fc9bd817f9712331a4a3e826f7bdc823e1d",
"version" : "0.9.1"
}
},
{ {
"identity" : "tree-sitter", "identity" : "tree-sitter",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/tree-sitter/tree-sitter", "location" : "https://github.com/tree-sitter/tree-sitter",
"state" : { "state" : {
"revision" : "98be227227af10cc7a269cb3ffb23686c0610b17", "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406",
"version" : "0.20.9" "version" : "0.25.10"
} }
} }
], ],