Improve JSON editor for macOS
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[submodule "Frameworks/Runestone"]
|
||||
path = Frameworks/Runestone
|
||||
url = https://github.com/nekohasekai/Runestone.git
|
||||
@@ -0,0 +1 @@
|
||||
--exclude Frameworks
|
||||
@@ -11,20 +11,65 @@ import SwiftUI
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
NavigationStackCompat {
|
||||
Group {
|
||||
if configuration.isLoading {
|
||||
ProgressView()
|
||||
} else {
|
||||
listContent
|
||||
#if os(macOS)
|
||||
macOSBody
|
||||
#else
|
||||
iOSBody
|
||||
#endif
|
||||
}
|
||||
|
||||
#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")
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
.toolbar {
|
||||
#if os(iOS) || os(tvOS)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .destructiveAction) {
|
||||
Button("Reset", role: .destructive) {
|
||||
Task {
|
||||
await configuration.resetToDefault()
|
||||
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) {
|
||||
Button("Reset", role: .destructive) {
|
||||
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 {
|
||||
List {
|
||||
|
||||
@@ -216,7 +216,7 @@ public struct ProfileCard: View {
|
||||
|
||||
private var manageProfilesSheet: some View {
|
||||
NavigationSheet(
|
||||
title: "Profiles",
|
||||
title: String(localized: "Manage profiles"),
|
||||
showDoneButton: true,
|
||||
onDismiss: { viewModel.showManageProfiles = false }
|
||||
) {
|
||||
@@ -227,11 +227,20 @@ public struct ProfileCard: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func editProfileSheet(for profile: Profile) -> some View {
|
||||
NavigationSheet(title: "Edit Profile") {
|
||||
EditProfileView()
|
||||
.environmentObject(profile)
|
||||
.environmentObject(environments)
|
||||
}
|
||||
#if os(macOS)
|
||||
NavigationSheet {
|
||||
EditProfileView()
|
||||
.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?
|
||||
|
||||
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 {
|
||||
EditProfileView()
|
||||
.environmentObject(profile)
|
||||
@@ -282,15 +300,28 @@ extension ProfileCard {
|
||||
createdProfile = profile
|
||||
}
|
||||
.environmentObject(environments)
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#if os(iOS) || os(tvOS)
|
||||
.presentationDetentsIfAvailable()
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
private var iOSBody: some View {
|
||||
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 {
|
||||
VStack {
|
||||
if viewModel.isLoading {
|
||||
ProgressView().onAppear {
|
||||
viewModel.setEnvironments(environments)
|
||||
Task {
|
||||
await viewModel.doReload()
|
||||
ProgressView()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.onAppear {
|
||||
viewModel.setEnvironments(environments)
|
||||
Task {
|
||||
await viewModel.doReload()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FormView {
|
||||
if viewModel.profileList.isEmpty {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
}
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.profileEditor) private var profileEditor
|
||||
|
||||
public var body: some View {
|
||||
viewBuilder {
|
||||
@@ -29,30 +30,10 @@
|
||||
}
|
||||
}
|
||||
} else {
|
||||
#if os(iOS)
|
||||
RunestoneTextView(
|
||||
text: readOnly ? .constant(viewModel.profileContent) : $viewModel.profileContent,
|
||||
isEditable: !readOnly
|
||||
)
|
||||
editorView
|
||||
.onChangeCompat(of: viewModel.profileContent) {
|
||||
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)
|
||||
@@ -105,6 +86,38 @@
|
||||
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
|
||||
|
||||
@@ -21,6 +21,14 @@
|
||||
isChanged = true
|
||||
}
|
||||
|
||||
public func reset() {
|
||||
isLoading = true
|
||||
profile = nil
|
||||
profileContent = ""
|
||||
isChanged = false
|
||||
alert = nil
|
||||
}
|
||||
|
||||
public func loadContent() async {
|
||||
do {
|
||||
try await loadContentBackground()
|
||||
|
||||
@@ -10,7 +10,16 @@ public struct EditProfileView: View {
|
||||
@StateObject private var viewModel = EditProfileViewModel()
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
#if os(macOS)
|
||||
macOSBody
|
||||
#else
|
||||
iOSBody
|
||||
#endif
|
||||
}
|
||||
|
||||
private var formContent: some View {
|
||||
FormView {
|
||||
FormItem(String(localized: "Name")) {
|
||||
TextField("Name", text: $profile.name, prompt: Text("Required"))
|
||||
@@ -60,48 +69,61 @@ public struct EditProfileView: View {
|
||||
ProfileActionToolbar(profile: profile, viewModel: viewModel)
|
||||
#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 {
|
||||
#if os(macOS)
|
||||
macOSBody
|
||||
#else
|
||||
iOSBody
|
||||
#endif
|
||||
}
|
||||
|
||||
private var formContent: some View {
|
||||
FormView {
|
||||
FormItem(String(localized: "Name")) {
|
||||
TextField("Name", text: $viewModel.profileName, prompt: Text("Required"))
|
||||
@@ -113,8 +121,19 @@ public struct NewProfileView: View {
|
||||
}
|
||||
#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 {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
@@ -138,10 +157,8 @@ public struct NewProfileView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.disabled(viewModel.isSaving)
|
||||
.alertBinding($viewModel.alert)
|
||||
#if os(iOS) || os(macOS)
|
||||
.fileImporter(
|
||||
isPresented: $viewModel.pickerPresented,
|
||||
allowedContentTypes: [.json],
|
||||
@@ -157,6 +174,30 @@ public struct NewProfileView: View {
|
||||
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 {
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
#if os(macOS)
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
#endif
|
||||
@ObservedObject private var profile: Profile
|
||||
@ObservedObject private var viewModel: EditProfileViewModel
|
||||
|
||||
@@ -69,16 +72,12 @@ public struct ProfileActionToolbar: View {
|
||||
|
||||
HStack(spacing: 12) {
|
||||
if profile.type != .remote {
|
||||
NavigationLink {
|
||||
EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: false))
|
||||
} label: {
|
||||
Text("Edit Content")
|
||||
Button("Edit Content") {
|
||||
openWindow(value: EditProfileContentView.Context(profileID: profile.id!, readOnly: false))
|
||||
}
|
||||
} else {
|
||||
NavigationLink {
|
||||
EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: true))
|
||||
} label: {
|
||||
Text("View Content")
|
||||
Button("View Content") {
|
||||
openWindow(value: EditProfileContentView.Context(profileID: profile.id!, readOnly: true))
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -92,14 +91,27 @@ public struct ProfileActionToolbar: View {
|
||||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Delete", role: .destructive) {
|
||||
Task {
|
||||
await viewModel.deleteProfile(profile, environments: environments, dismiss: dismiss)
|
||||
}
|
||||
}
|
||||
.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()
|
||||
.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
|
||||
public struct NavigationSheet<Content: View>: View {
|
||||
private let title: String
|
||||
private let title: String?
|
||||
private let size: SheetSize
|
||||
private let showDoneButton: Bool
|
||||
private let onDismiss: (() -> Void)?
|
||||
private let content: () -> Content
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
title: String? = nil,
|
||||
size: SheetSize = .large,
|
||||
showDoneButton: Bool = false,
|
||||
onDismiss: (() -> Void)? = nil,
|
||||
@@ -39,13 +39,25 @@ public struct NavigationSheet<Content: View>: View {
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
NavigationStackCompat {
|
||||
content()
|
||||
.navigationTitle(title)
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
#if os(macOS)
|
||||
#if os(macOS)
|
||||
macOSBody
|
||||
#else
|
||||
iOSBody
|
||||
#endif
|
||||
}
|
||||
|
||||
#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 {
|
||||
if showDoneButton {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
@@ -55,12 +67,19 @@ public struct NavigationSheet<Content: View>: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(iOS) || os(tvOS)
|
||||
.sheetDetent(size)
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
private var iOSBody: some View {
|
||||
NavigationStackCompat {
|
||||
content()
|
||||
.navigationTitle(title ?? "")
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
.sheetDetent(size)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#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
|
||||
Submodule
+1
Submodule Frameworks/Runestone added at b3ef61c54b
@@ -13,7 +13,7 @@ let package = Package(
|
||||
.library(name: "TreeSitterJSON5Runestone", targets: ["TreeSitterJSON5Runestone"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/simonbs/Runestone", from: "0.3.0"),
|
||||
.package(path: "../Runestone"),
|
||||
],
|
||||
targets: [
|
||||
.target(name: "TreeSitterJSON5", cSettings: [.headerSearchPath("src")]),
|
||||
|
||||
@@ -195,6 +195,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Cancel" : {
|
||||
|
||||
},
|
||||
"Chain" : {
|
||||
"localizations" : {
|
||||
@@ -507,6 +510,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Do you want to save the changes you made?" : {
|
||||
|
||||
},
|
||||
"Documentation" : {
|
||||
"localizations" : {
|
||||
@@ -527,6 +533,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Don't Save" : {
|
||||
|
||||
},
|
||||
"Done" : {
|
||||
"localizations" : {
|
||||
@@ -1017,6 +1026,9 @@
|
||||
},
|
||||
"Machine: " : {
|
||||
"shouldTranslate" : false
|
||||
},
|
||||
"Manage profiles" : {
|
||||
|
||||
},
|
||||
"Match Rule" : {
|
||||
"shouldTranslate" : false
|
||||
@@ -1295,6 +1307,7 @@
|
||||
}
|
||||
},
|
||||
"Profiles" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
@@ -1746,6 +1759,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Unsaved Changes" : {
|
||||
|
||||
},
|
||||
"Update" : {
|
||||
"localizations" : {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ public struct MacApplication: Scene {
|
||||
@State private var isMenuPresented = false
|
||||
@StateObject private var environments = ExtensionEnvironments()
|
||||
|
||||
private let profileEditor: (Binding<String>, Bool) -> AnyView = { text, isEditable in
|
||||
AnyView(CodeEditTextView(text: text, isEditable: isEditable))
|
||||
}
|
||||
|
||||
public init() {}
|
||||
public var body: some Scene {
|
||||
Window("sing-box", id: "main", content: {
|
||||
@@ -52,6 +56,13 @@ public struct MacApplication: Scene {
|
||||
}
|
||||
.menuBarExtraStyle(.window)
|
||||
.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 {
|
||||
|
||||
@@ -10,6 +10,10 @@ public struct MainView: View {
|
||||
@State private var showCardManagement = false
|
||||
@State private var cardConfigurationVersion = 0
|
||||
|
||||
private let profileEditor: (Binding<String>, Bool) -> AnyView = { text, isEditable in
|
||||
AnyView(CodeEditTextView(text: text, isEditable: isEditable))
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
@@ -59,6 +63,7 @@ public struct MainView: View {
|
||||
.environment(\.selection, $viewModel.selection)
|
||||
.environment(\.importProfile, $viewModel.importProfile)
|
||||
.environment(\.importRemoteProfile, $viewModel.importRemoteProfile)
|
||||
.environment(\.profileEditor, profileEditor)
|
||||
.handlesExternalEvents(preferring: [], allowing: ["*"])
|
||||
.onOpenURL(perform: viewModel.openURL)
|
||||
.sheet(isPresented: $showCardManagement) {
|
||||
|
||||
@@ -16,6 +16,10 @@ struct MainView: View {
|
||||
@State private var showConnections = false
|
||||
@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 {
|
||||
guard !environments.extensionProfileLoading else {
|
||||
return false
|
||||
@@ -124,6 +128,7 @@ struct MainView: View {
|
||||
.environment(\.selection, $selection)
|
||||
.environment(\.importProfile, $importProfile)
|
||||
.environment(\.importRemoteProfile, $importRemoteProfile)
|
||||
.environment(\.profileEditor, profileEditor)
|
||||
.handlesExternalEvents(preferring: [], allowing: ["*"])
|
||||
.onOpenURL(perform: openURL)
|
||||
.sheet(isPresented: $showGroups) {
|
||||
@@ -160,6 +165,7 @@ struct MainView: View {
|
||||
.environment(\.selection, $selection)
|
||||
.environment(\.importProfile, $importProfile)
|
||||
.environment(\.importRemoteProfile, $importRemoteProfile)
|
||||
.environment(\.profileEditor, profileEditor)
|
||||
.handlesExternalEvents(preferring: [], allowing: ["*"])
|
||||
.onOpenURL(perform: openURL)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
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, ); }; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
@@ -536,8 +537,6 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3A4EAD1B2A4FEB02005435B3 /* Library.framework in Frameworks */,
|
||||
3A2E87FB2ED5ABDA00644195 /* TreeSitterJSON5Runestone in Frameworks */,
|
||||
3A2E87F22ED5A91100644195 /* Runestone in Frameworks */,
|
||||
3A4A020D2B53E3DC004EFB87 /* QRCode in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -585,6 +584,8 @@
|
||||
files = (
|
||||
3A9759202A4EB69C00E4404B /* Library.framework in Frameworks */,
|
||||
3A4EAD372A4FEC20005435B3 /* ApplicationLibrary.framework in Frameworks */,
|
||||
3A2E87F22ED5A91100644195 /* Runestone in Frameworks */,
|
||||
3A2E87FB2ED5ABDA00644195 /* TreeSitterJSON5Runestone in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -633,6 +634,7 @@
|
||||
files = (
|
||||
3AEECC452A6DFE61006A0E0C /* ApplicationLibrary.framework in Frameworks */,
|
||||
3AEECC4E2A6DFF13006A0E0C /* MacControlCenterUI in Frameworks */,
|
||||
3ACE5E032EE1A91300644196 /* CodeEditSourceEditor in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -758,8 +760,6 @@
|
||||
name = ApplicationLibrary;
|
||||
packageProductDependencies = (
|
||||
3A4A020C2B53E3DC004EFB87 /* QRCode */,
|
||||
3A2E87F12ED5A91100644195 /* Runestone */,
|
||||
3A2E87FA2ED5ABDA00644195 /* TreeSitterJSON5Runestone */,
|
||||
);
|
||||
productName = ApplicationLibrary;
|
||||
productReference = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */;
|
||||
@@ -872,6 +872,8 @@
|
||||
);
|
||||
name = SFI;
|
||||
packageProductDependencies = (
|
||||
3A2E87F12ED5A91100644195 /* Runestone */,
|
||||
3A2E87FA2ED5ABDA00644195 /* TreeSitterJSON5Runestone */,
|
||||
);
|
||||
productName = SFI;
|
||||
productReference = 3AEC20F32A459AB400A63465 /* sing-box.app */;
|
||||
@@ -992,6 +994,7 @@
|
||||
name = MacLibrary;
|
||||
packageProductDependencies = (
|
||||
3AEECC4D2A6DFF13006A0E0C /* MacControlCenterUI */,
|
||||
3ACE5E022EE1A91200644196 /* CodeEditSourceEditor */,
|
||||
);
|
||||
productName = MacLibrary;
|
||||
productReference = 3AEECC2F2A6DFDAD006A0E0C /* MacLibrary.framework */;
|
||||
@@ -1064,8 +1067,9 @@
|
||||
3A017F902A4AB2E4009149FA /* XCRemoteSwiftPackageReference "GRDB" */,
|
||||
3A57DF3A2A4D705000690BC5 /* XCRemoteSwiftPackageReference "MacControlCenterUI" */,
|
||||
3A4A020B2B53E3DC004EFB87 /* XCRemoteSwiftPackageReference "qrcode" */,
|
||||
3A2E87F02ED5A91100644195 /* XCRemoteSwiftPackageReference "Runestone" */,
|
||||
3A2E87F02ED5A91100644195 /* XCLocalSwiftPackageReference "Frameworks/Runestone" */,
|
||||
3A2E87F92ED5ABCF00644195 /* XCLocalSwiftPackageReference "Frameworks/TreeSitterJSON5" */,
|
||||
3ACE5E012EE1A91100644196 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */,
|
||||
);
|
||||
productRefGroup = 3AEC20C72A45991900A63465 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -2562,6 +2566,10 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
3A2E87F02ED5A91100644195 /* XCLocalSwiftPackageReference "Frameworks/Runestone" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Frameworks/Runestone;
|
||||
};
|
||||
3A2E87F92ED5ABCF00644195 /* XCLocalSwiftPackageReference "Frameworks/TreeSitterJSON5" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Frameworks/TreeSitterJSON5;
|
||||
@@ -2577,14 +2585,6 @@
|
||||
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" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/dagronf/qrcode.git";
|
||||
@@ -2609,6 +2609,14 @@
|
||||
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 */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -2619,7 +2627,7 @@
|
||||
};
|
||||
3A2E87F12ED5A91100644195 /* Runestone */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 3A2E87F02ED5A91100644195 /* XCRemoteSwiftPackageReference "Runestone" */;
|
||||
package = 3A2E87F02ED5A91100644195 /* XCLocalSwiftPackageReference "Frameworks/Runestone" */;
|
||||
productName = Runestone;
|
||||
};
|
||||
3A2E87FA2ED5ABDA00644195 /* TreeSitterJSON5Runestone */ = {
|
||||
@@ -2637,6 +2645,11 @@
|
||||
package = 3A7E90362A46778E00D53052 /* XCRemoteSwiftPackageReference "BinaryCodable" */;
|
||||
productName = BinaryCodable;
|
||||
};
|
||||
3ACE5E022EE1A91200644196 /* CodeEditSourceEditor */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 3ACE5E012EE1A91100644196 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */;
|
||||
productName = CodeEditSourceEditor;
|
||||
};
|
||||
3AEECC4D2A6DFF13006A0E0C /* MacControlCenterUI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 3A57DF3A2A4D705000690BC5 /* XCRemoteSwiftPackageReference "MacControlCenterUI" */;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "48cbd083120736a1a1ce4b717dcbfdb7900aa5e2aa21991865021fb98adc2093",
|
||||
"originHash" : "3ae592e476adf467b19c2cde1d4ba44683fb6f05a27fc1959105b8eb14cce1d2",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "binarycodable",
|
||||
@@ -10,6 +10,42 @@
|
||||
"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",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -47,12 +83,21 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "runestone",
|
||||
"identity" : "rearrange",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/simonbs/Runestone",
|
||||
"location" : "https://github.com/ChimeHQ/Rearrange",
|
||||
"state" : {
|
||||
"revision" : "1fad339aab99cf2136ce6bf8c32da3265b2e85e5",
|
||||
"version" : "0.5.1"
|
||||
"revision" : "f1d74e1642956f0300756ad8d1d64e9034857bc3",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tree-sitter/tree-sitter",
|
||||
"state" : {
|
||||
"revision" : "98be227227af10cc7a269cb3ffb23686c0610b17",
|
||||
"version" : "0.20.9"
|
||||
"revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406",
|
||||
"version" : "0.25.10"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user