diff --git a/.github/scripts/tests.sh b/.github/scripts/tests.sh index 57afd27..8bc6b19 100755 --- a/.github/scripts/tests.sh +++ b/.github/scripts/tests.sh @@ -16,4 +16,5 @@ export LC_CTYPE=en_US.UTF-8 set -o pipefail && arch -"${ARCH}" xcodebuild \ -scheme CodeEditKit \ -destination "platform=macos,arch=${ARCH}" \ + -skipPackagePluginValidation \ clean test | xcpretty diff --git a/.swiftlint.yml b/.swiftlint.yml index 8c2e0c0..ecb6b36 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,9 +1,10 @@ disabled_rules: - - todo - - trailing_comma - - nesting + - force_cast + +line_length: 160 type_name: + allowed_symbols: ['_'] excluded: - ID @@ -11,6 +12,4 @@ identifier_name: min_length: 2 allowed_symbols: ['_'] excluded: - - c - id - - vc diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..1d539eb --- /dev/null +++ b/Package.resolved @@ -0,0 +1,95 @@ +{ + "pins" : [ + { + "identity" : "anycodable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Flight-School/AnyCodable", + "state" : { + "revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05", + "version" : "0.6.7" + } + }, + { + "identity" : "concurrencyplus", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/ConcurrencyPlus", + "state" : { + "branch" : "main", + "revision" : "8dc56499412a373d617d50d059116bccf44b9874" + } + }, + { + "identity" : "fseventswrapper", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Frizlab/FSEventsWrapper", + "state" : { + "revision" : "e0c59a2ce2775e5f6642da6d19207445f10112d0", + "version" : "1.0.2" + } + }, + { + "identity" : "glob", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Bouke/Glob", + "state" : { + "revision" : "deda6e163d2ff2a8d7e138e2c3326dbd71157faf", + "version" : "1.0.5" + } + }, + { + "identity" : "jsonrpc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/JSONRPC", + "state" : { + "revision" : "afc20d00e38674774f84edc325424a32ae3b9e01", + "version" : "0.7.0" + } + }, + { + "identity" : "languageclient", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/LanguageClient", + "state" : { + "revision" : "92beeecd0bb783da52227839ba6c55e43fc866ec", + "version" : "0.5.1" + } + }, + { + "identity" : "languageserverprotocol", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/LanguageServerProtocol", + "state" : { + "revision" : "192bcfdcf7a013da49c6fa1b95de66254ce7c614", + "version" : "0.9.1" + } + }, + { + "identity" : "processenv", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/ProcessEnv", + "state" : { + "revision" : "29487b6581bb785c372c611c943541ef4309d051", + "version" : "0.3.1" + } + }, + { + "identity" : "processservice", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/ProcessService", + "state" : { + "revision" : "369fb0379983d3b43c7d7ad62c4e91ee020e347c", + "version" : "0.2.6" + } + }, + { + "identity" : "swiftlintplugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/lukepistrol/SwiftLintPlugin", + "state" : { + "revision" : "d3ec7fb242ebe1d8e23bf17e58a1e27d43125994", + "version" : "0.2.6" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift index 499ad18..311b985 100644 --- a/Package.swift +++ b/Package.swift @@ -1,22 +1,39 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.7 import PackageDescription let package = Package( name: "CodeEditKit", + platforms: [ + .macOS(.v13) + ], products: [ .library( name: "CodeEditKit", type: .dynamic, - targets: ["CodeEditKit"]), + targets: ["CodeEditKit"]) + ], + dependencies: [ + .package(url: "https://github.com/ChimeHQ/ConcurrencyPlus", branch: "main"), + .package(url: "https://github.com/ChimeHQ/LanguageClient", from: "0.5.0"), + .package(url: "https://github.com/ChimeHQ/ProcessService", from: "0.2.6"), + .package( + url: "https://github.com/lukepistrol/SwiftLintPlugin", + from: "0.2.2" + ), + .package( + url: "https://github.com/Flight-School/AnyCodable", + from: "0.6.0" + ) ], - dependencies: [], targets: [ .target( name: "CodeEditKit", - dependencies: []), + dependencies: ["AnyCodable", "ConcurrencyPlus", "LanguageClient", .product(name: "ProcessServiceClient", package: "ProcessService")], + plugins: [.plugin(name: "SwiftLint", package: "SwiftLintPlugin")] + ), .testTarget( name: "CodeEditKitTests", - dependencies: ["CodeEditKit"]), + dependencies: ["CodeEditKit"]) ] ) diff --git a/Sources/CodeEditKit/CodableColorArray.swift b/Sources/CodeEditKit/CodableColorArray.swift new file mode 100644 index 0000000..5b8d12b --- /dev/null +++ b/Sources/CodeEditKit/CodableColorArray.swift @@ -0,0 +1,39 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 30/12/2022. +// + +import Foundation +import AppKit + +@propertyWrapper +public struct CodableColorArray { + public var wrappedValue: [NSColor] + + public init(wrappedValue: [NSColor]) { + self.wrappedValue = wrappedValue + } +} + +extension CodableColorArray: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let data = try container.decode(Data.self) + + guard let color = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: NSColor.self, from: data) else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Invalid color" + ) + } + wrappedValue = color + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + let data = try NSKeyedArchiver.archivedData(withRootObject: wrappedValue, requiringSecureCoding: true) + try container.encode(data) + } +} diff --git a/Sources/CodeEditKit/CodableWrappers.swift b/Sources/CodeEditKit/CodableWrappers.swift new file mode 100644 index 0000000..fab11f2 --- /dev/null +++ b/Sources/CodeEditKit/CodableWrappers.swift @@ -0,0 +1,52 @@ +// +// File 2.swift +// +// +// Created by Wouter Hennen on 06/12/2022. +// + +import Foundation + +@propertyWrapper +public struct Encoded { + public var wrappedValue: T + + public init(wrappedValue: T) { + self.wrappedValue = wrappedValue + } + + public var errorDescription: String? { + do { + _ = try JSONEncoder().encode(wrappedValue) + return nil + } catch { + return error.localizedDescription + } + } + + public var projectedValue: Data? { + try? JSONEncoder().encode(wrappedValue) + } +} + +@propertyWrapper +public struct Decoded { + public var wrappedValue: Data + + public init(wrappedValue: Data) { + self.wrappedValue = wrappedValue + } + + public var errorDescription: String? { + do { + _ = try JSONDecoder().decode(T.self, from: wrappedValue) + return nil + } catch { + return error.localizedDescription + } + } + + public var projectedValue: T? { + try? JSONDecoder().decode(T.self, from: wrappedValue) + } +} diff --git a/Sources/CodeEditKit/CodeEditExtension+Body.swift b/Sources/CodeEditKit/CodeEditExtension+Body.swift new file mode 100644 index 0000000..b0a3b33 --- /dev/null +++ b/Sources/CodeEditKit/CodeEditExtension+Body.swift @@ -0,0 +1,41 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 03/01/2023. +// + +import Foundation +import ExtensionKit + +struct EmptyAppExtensionScene: AppExtensionScene { + var body: Never { + fatalError() + } +} + +public extension CodeEditExtension { + var body: some AppExtensionScene { + EmptyAppExtensionScene() + } +} + +public extension CodeEditExtension where Self: SettingsExtension { + var body: some AppExtensionScene { + settingsScene + } +} + +public extension CodeEditExtension where Self: SidebarExtension { + var body: some AppExtensionScene { + sidebarScenes + } +} + +public extension CodeEditExtension where Self: SettingsExtension & SidebarExtension { + @AppExtensionSceneBuilder + var body: some AppExtensionScene { + settingsScene + sidebarScenes + } +} diff --git a/Sources/CodeEditKit/CodeEditExtension.swift b/Sources/CodeEditKit/CodeEditExtension.swift new file mode 100644 index 0000000..cd5e317 --- /dev/null +++ b/Sources/CodeEditKit/CodeEditExtension.swift @@ -0,0 +1,77 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 30/12/2022. +// + +import ExtensionKit +import ExtensionFoundation + +public protocol CodeEditExtension: AppExtension { + + /// UI scenes of the extension. + associatedtype Body: AppExtensionScene + + /// Extension Configuration. + associatedtype Configuration = AppExtensionSceneConfiguration + + /// A brief description of the extension. Should be max a few words. + var description: String { get } + + /// A list of Entitlements the app needs, e.g. Network Access. + var entitlements: [Entitlement] { get } + + /// UI scenes of the extension. + /// Use the default implementation. + var body: Body { get } +} + +extension CodeEditExtension { + + var extensionURL: URL { + Bundle.main.bundleURL + } + + func gatherAvailableExtensions() -> [ExtensionKind] { + var extensions: [ExtensionKind] = [] + + if self is any SettingsExtension { + extensions.append(.settings) + } + + if let self = self as? any SidebarExtension { + extensions.append(contentsOf: self.availableExtensions) + } + + return extensions + } +} + +public extension CodeEditExtension where Self: AnyObject { + /// XPC Configuration for communication with CodeEdit. + /// Use the default implementation. + var configuration: AppExtensionSceneConfiguration { + AppExtensionSceneConfiguration(self.body, configuration: SettingsExtensionConfiguration(self)) + } +} + +struct SettingsExtensionConfiguration: AppExtensionConfiguration { + public func accept(connection: NSXPCConnection) -> Bool { + guard let appExtension else { + return false + } + + connection.exportedInterface = .init(with: XPCWrappable.self) + connection.exportedObject = XPCWrapper(appExtension) + connection.resume() + + return true + } + + weak var appExtension: E? + + public init(_ appExtension: E) { + self.appExtension = appExtension + } +} diff --git a/Sources/CodeEditKit/Debugging/Logging.swift b/Sources/CodeEditKit/Debugging/Logging.swift new file mode 100644 index 0000000..9171113 --- /dev/null +++ b/Sources/CodeEditKit/Debugging/Logging.swift @@ -0,0 +1,13 @@ +// +// Logging.swift +// +// +// Created by Wouter Hennen on 22/05/2023. +// + +import Foundation + +public func print(_ items: Any..., separator: String = " ", terminator: String = "\n") { + let formedString = items.map { String(describing: $0) }.joined(separator: separator) + NSLog(formedString) +} diff --git a/Sources/CodeEditKit/Documentation.docc/CodeEditExtension.md b/Sources/CodeEditKit/Documentation.docc/CodeEditExtension.md new file mode 100644 index 0000000..a201c4e --- /dev/null +++ b/Sources/CodeEditKit/Documentation.docc/CodeEditExtension.md @@ -0,0 +1,57 @@ +# ``CodeEditKit/CodeEditExtension`` + +@Metadata { + @DocumentationExtension(mergeBehavior: append) +} + +The base extension. + +## Overview + +All extensions supported by CodeEdit are defined as a ``CodeEditExtension``. +This protocol provides basic information about the extension, such as the ``description``, name and ``entitlements``. + +This type is the entrypoint of your extension, so it should be marked with `@main`. + +Additional extensions can be defined by extending the class that implements ``CodeEditExtension``. + +## Example + +```swift +@main +final class ExampleExtension: CodeEditExtension { + var description: String = "test" + + var entitlements: [Entitlement] = [.currentfile] +} + +// Add a Settings Pane +extension ExampleExtension: SettingsExtension { + var settings: some View { + SettingsView() + } +} + +// The Settings View +struct SettingsView: View { + @State var enabled = false + var body: some View { + Toggle("Enabled", isOn: $enabled) + } +} +``` + +> Info: ``CodeEditExtension`` Conforms to ObservableObject and is automatically injected into the environment of each View. This way, it's possible to have shared state between different UI extensions by observing the object in a View. +> ```swift +> struct AView: View { +> @EnvironmentObject var main: ExampleExtension +> } +> ``` + + + +## Topics + +### Group + +- ``Symbol`` diff --git a/Sources/CodeEditKit/Documentation.docc/Commands.md b/Sources/CodeEditKit/Documentation.docc/Commands.md new file mode 100644 index 0000000..1652e2b --- /dev/null +++ b/Sources/CodeEditKit/Documentation.docc/Commands.md @@ -0,0 +1,13 @@ +# Commands + +Quick actions + +## Overview + +Blablabla + +## Topics + +### Group + +- ``Symbol`` diff --git a/Sources/CodeEditKit/Documentation.docc/CreatingProject.md b/Sources/CodeEditKit/Documentation.docc/CreatingProject.md new file mode 100644 index 0000000..7faaed9 --- /dev/null +++ b/Sources/CodeEditKit/Documentation.docc/CreatingProject.md @@ -0,0 +1,23 @@ +# Project Setup + +## Overview + +To start developing extensions, you first need to configure your project. + +## Steps + +This is a WIP, and should be explained better. + +1. Create a new Xcode project. As the template, choose "App" (make sure you select the macOS tab, not the multiplatform tab) +2. Once created, go to File -> New -> Target. In the macOS tab, choose "Generic Extension". Enter a name, enable the checkbox for UI support, and leave the other things default. Click "activate" on the popup. +3. In the folder of your new extension, a new "Info" plist file should have been created. In it, replace the value of EXExtensionPointIdentifier with "codeedit.extension" (no quotation marks) +4. Next, add CodeEditKit as a dependency. If you have CodeEditKit as a local package, first make sure no other Xcode projects that also rely on that package are opened. Otherwise, Xcode won't find the package. +5. Go to the project configuration, select the target with the name of your extension, and go to the general tab. There, add CodeEditKit to the "Frameworks and Libraries" section. +6. Now, go to the main swift file in the extension folder. Remove all autogenerated code, we won't need it. Instead, import CodeEditKit and follow the steps for ``CodeEditExtension``. +7. Lastly, press run! The extension will compile and Xcode will ask which app to start. Choose CodeEdit. To prevent this popup from always showing, you can edit the extension scheme to always choose CodeEdit. + +## Topics + +### Group + +- ``Symbol`` diff --git a/Sources/CodeEditKit/Documentation.docc/Documentation.md b/Sources/CodeEditKit/Documentation.docc/Documentation.md index 36bb499..df45b16 100644 --- a/Sources/CodeEditKit/Documentation.docc/Documentation.md +++ b/Sources/CodeEditKit/Documentation.docc/Documentation.md @@ -4,6 +4,45 @@ CodeEditKit is a dynamic library which is shared between CodeEdit and it's exten ## Overview -This is still work in progress. +Extensions are an essential part of CodeEdit. They can extend the languages CodeEdit supports, add custom actions and behaviors, and even provide custom views of certain data. -> See [this thread](https://github.com/orgs/CodeEditApp/discussions/792) for more information. +CodeEditKit aims to provide an easy and straightforward API to implement these extensions, with modern technologies like Swift, SwiftUI and ExtensionKit. + +### Extension Types + +There are lots of extension types you can use to extend the functionality of CodeEdit. + +#### General Extensions +- (WIP) +- (Not Started) + +#### Language Extensions +- (Not Started) +- (WIP) + +#### UI Extensions +- ``SidebarExtension`` (Beta) +- (WIP) +- (Beta) + +### Getting Started +To setup your Xcode project, have a look at the section. + +Next, have a look at ``CodeEditExtension``. This is the base protocol that will define the main structure of your extension. +All extensions that you'll add will extend (:p) this type. + +Finally, try adding an extension to your newly created type. A good first recommendation is the extension, as you'll likely need this later. + +### Development & Debugging +Developing extensions with ExtensionKit has a few annoyances: +- Prints do not appear in the Xcode console. Instead, the debug information of CodeEdit (not the extension) is printed. +- Modifying extension requires CodeEdit reload. +- An extension needs to be sandboxed. + +CodeEdit tries to overcome these issues by offering a few solutions. + +First, CodeEdit is able to capture log messages (which are also sent to Console.app). Therefore, CodeEdit provides a custom ``print(_:separator:terminator:)`` function which works the same as Swift's one, except it sends its input as an OSLog instead of printing it to stdout. With this change, you are able to view your extensions' outputs in CodeEdit's console. Later on, an extra toggle will be provided which will also print the changes to Xcode's console. + +Additionally, if you choose to use the powerful Logger system to debug, CodeEdit provides support for various log types and will color them accordingly. + +Next, CodeEdit supports a way of hot reloading your extension. Instead of relaunching CodeEdit and the extension, only the extension is restarted, resulting in faster load times. To make use of this, build your extension instead of running it (⌘B instead of ⌘R). Note that this feature is still in beta and may behave unexpectedly. diff --git a/Sources/CodeEditKit/Documentation.docc/GenericExtension.md b/Sources/CodeEditKit/Documentation.docc/GenericExtension.md new file mode 100644 index 0000000..507d3bb --- /dev/null +++ b/Sources/CodeEditKit/Documentation.docc/GenericExtension.md @@ -0,0 +1,15 @@ +# ``CodeEditKit/GenericExtension`` + +Summary + +## Overview + +Text + +> Important: If you instantiate multiple instances of a struct that implements ``GenericExtension``, replace the default implementation of ``id`` with an unique ``id``. Otherwise, they'll have the same ``id`` which will cause trouble. + +## Topics + +### Group + +- ``Symbol`` diff --git a/Sources/CodeEditKit/Documentation.docc/LanguageServers.md b/Sources/CodeEditKit/Documentation.docc/LanguageServers.md new file mode 100644 index 0000000..6f4fd35 --- /dev/null +++ b/Sources/CodeEditKit/Documentation.docc/LanguageServers.md @@ -0,0 +1,13 @@ +# Language Servers + +Summary + +## Overview + +Text + +## Topics + +### Group + +- ``Symbol`` diff --git a/Sources/CodeEditKit/Documentation.docc/Settings.md b/Sources/CodeEditKit/Documentation.docc/Settings.md new file mode 100644 index 0000000..2826d1c --- /dev/null +++ b/Sources/CodeEditKit/Documentation.docc/Settings.md @@ -0,0 +1,43 @@ +# Settings + +Summary + +## Overview + +The ``SettingsExtension`` allows an extension to provide additional settings to the settings window of CodeEdit. +Extension-specific settings can be configured this way. + +``SettingsExtension`` expects a SwiftUI View and doesn't take care of the storage of settings. Therefore, you are responsible for storing your extensions' settings in an appropriate manner. + +## Example + +```swift +@main +final class SettingsExampleExtension: CodeEditExtension { + + var description: String = "" + + var entitlements: [Entitlement] = [.currentfile] + +} + +extension SettingsExampleExtension: SettingsExtension { + var settings: some View { + SettingsView() + } +} + +struct SettingsView: View { + @AppStorage("appstoragekey") var appstoragekey = false + + var body: some View { + Toggle("App Storage Key", isOn: $appstoragekey) + } +} +``` + +## Topics + +### Group + +- ``Symbol`` diff --git a/Sources/CodeEditKit/Documentation.docc/Sidebars.md b/Sources/CodeEditKit/Documentation.docc/Sidebars.md new file mode 100644 index 0000000..b646d6a --- /dev/null +++ b/Sources/CodeEditKit/Documentation.docc/Sidebars.md @@ -0,0 +1,54 @@ +# ``CodeEditKit/SidebarExtension`` + +@Metadata { + @DocumentationExtension(mergeBehavior: append) +} + +Navigator & Inspector UI Extensions + +## Overview + +The ``SidebarExtension`` protocol takes care of Navigator and Inspector extensions. These extensions are added to the tabbar of each sidebar, and can be selected by the user. + +## Example + +```swift +@main +final class SidebarExtensionExample: CodeEditExtension { + var description: String = "" + + var entitlements: [Entitlement] = [] +} + +extension SidebarExtensionExample: SidebarExtension { + var sidebars: some Sidebar { + Inspector(id: "umbrellaInspector") { + Form { + Text("Hello, world!") + } + .formStyle(.grouped) + } + .help("Umbrella Inspector") + .icon("umbrella") + + Navigator(id: "carrotNavigator") { + Form { + Text("Hello, world!") + } + .formStyle(.grouped) + } + .help("Carrot Navigator") + .icon("carrot") + } +} + +``` + +## Topics + +### Sidebar Modifiers + +- ``Sidebar/help(_:)`` +- ``Sidebar/icon(_:)`` +- ``Sidebar/description(_:)`` +- ``Sidebar/title(_:)`` diff --git a/Sources/CodeEditKit/Documentation.docc/Snippets.md b/Sources/CodeEditKit/Documentation.docc/Snippets.md new file mode 100644 index 0000000..12e8276 --- /dev/null +++ b/Sources/CodeEditKit/Documentation.docc/Snippets.md @@ -0,0 +1,13 @@ +# Snippets + +Summary + +## Overview + +Text + +## Topics + +### Group + +- ``Symbol`` diff --git a/Sources/CodeEditKit/Documentation.docc/Themes.md b/Sources/CodeEditKit/Documentation.docc/Themes.md new file mode 100644 index 0000000..244f67c --- /dev/null +++ b/Sources/CodeEditKit/Documentation.docc/Themes.md @@ -0,0 +1,13 @@ +# Themes + +Summary + +## Overview + +Text + +## Topics + +### Group + +- ``Symbol`` diff --git a/Sources/CodeEditKit/Documentation.docc/ToolbarItems.md b/Sources/CodeEditKit/Documentation.docc/ToolbarItems.md new file mode 100644 index 0000000..f0155e0 --- /dev/null +++ b/Sources/CodeEditKit/Documentation.docc/ToolbarItems.md @@ -0,0 +1,13 @@ +# Toolbar Items + +Summary + +## Overview + +Text + +## Topics + +### Group + +- ``Symbol`` diff --git a/Sources/CodeEditKit/Entitlements.swift b/Sources/CodeEditKit/Entitlements.swift new file mode 100644 index 0000000..8505e3c --- /dev/null +++ b/Sources/CodeEditKit/Entitlements.swift @@ -0,0 +1,22 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 30/12/2022. +// + +import Foundation + +public enum Entitlement: CustomStringConvertible, Codable, CaseIterable { + case workspace + case currentfile + + public var description: String { + switch self { + case .workspace: + return "Workspace" + case .currentfile: + return "Current File" + } + } +} diff --git a/Sources/CodeEditKit/ExtensionAPI.swift b/Sources/CodeEditKit/ExtensionAPI.swift deleted file mode 100644 index 12ab798..0000000 --- a/Sources/CodeEditKit/ExtensionAPI.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ExtensionAPI.swift -// -// -// Created by Pavel Kasila on 27.03.22. -// - -import Foundation - -/// A protocol to conform to for Extension API instance assigned to ``extensionId`` -public protocol ExtensionAPI { - - var extensionId: String { get } - var workspaceURL: URL { get } - - /// API to work with targets - var targets: TargetsAPI { get } - -} diff --git a/Sources/CodeEditKit/ExtensionInterface.swift b/Sources/CodeEditKit/ExtensionInterface.swift deleted file mode 100644 index 2584a28..0000000 --- a/Sources/CodeEditKit/ExtensionInterface.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ExtensionInterface.swift -// -// -// Created by Pavel Kasila on 27.03.22. -// - -import Foundation - -/// A protocol for extensions to conform to -public protocol ExtensionInterface { -} - -open class ExtensionBuilder: NSObject { - required public override init() { - super.init() - } - - /// Builds extension with API - /// - Parameter withAPI: the API implementation itself - open func build(withAPI api: ExtensionAPI) -> ExtensionInterface { - fatalError("You should override ExtensionBuilder.build") - } -} diff --git a/Sources/CodeEditKit/ExtensionKind.swift b/Sources/CodeEditKit/ExtensionKind.swift new file mode 100644 index 0000000..c8b5e20 --- /dev/null +++ b/Sources/CodeEditKit/ExtensionKind.swift @@ -0,0 +1,28 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 30/12/2022. +// + +import SwiftUI + +public enum ExtensionKind: Codable, Hashable, CustomStringConvertible { + case sidebarItem(data: ResolvedSidebar.SidebarStore) + case action(actionID: String) + case theme(themeID: String) + case settings + + public var description: String { + switch self { + case .sidebarItem(let data): + return "Sidebar with ID \(data.sceneID)" + case .action(let actionID): + return "Action with ID \(actionID)" + case .theme(let themeID): + return "Theme with ID \(themeID)" + case .settings: + return "Settings" + } + } +} diff --git a/Sources/CodeEditKit/ExtensionManifest.swift b/Sources/CodeEditKit/ExtensionManifest.swift deleted file mode 100644 index a87c84f..0000000 --- a/Sources/CodeEditKit/ExtensionManifest.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ExtensionManifest.swift -// -// -// Created by Pavel Kasila on 27.03.22. -// - -import Foundation - -public struct ExtensionManifest: Codable, Hashable { - public var name: String - public var displayName: String - public var homepage: URL? - public var repository: URL? - public var issues: URL? -} diff --git a/Sources/CodeEditKit/GenericScene.swift b/Sources/CodeEditKit/GenericScene.swift new file mode 100644 index 0000000..3dc5e93 --- /dev/null +++ b/Sources/CodeEditKit/GenericScene.swift @@ -0,0 +1,44 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 02/01/2023. +// + +import SwiftUI +import ExtensionKit + +public enum Callbacks: Codable { + case openWindow(id: String) +} + +struct GenericScene: AppExtensionScene { + + var sceneID: String + + @ViewBuilder + var content: Content + + var environmentWrapper = EnvironmentPublisher() + + var connection: NSXPCConnection? + + var body: some AppExtensionScene { + PrimitiveAppExtensionScene(id: sceneID) { + GeneralSettingsView(environmentWrapper: environmentWrapper) { + content + .environment(\.ceOpenWindow) { id in + try await environmentWrapper.update(callback: .openWindow(id: id)) + } + } + } onConnection: { connection in + connection.exportedInterface = .init(with: EnvironmentPublisherObjc.self) + connection.remoteObjectInterface = .init(with: EnvironmentPublisherObjc.self) + connection.exportedObject = environmentWrapper + environmentWrapper.connection = connection + connection.resume() + + return true + } + } +} diff --git a/Sources/CodeEditKit/NonUIExtensions/LSP/LanguageExtension.swift b/Sources/CodeEditKit/NonUIExtensions/LSP/LanguageExtension.swift new file mode 100644 index 0000000..cc93c22 --- /dev/null +++ b/Sources/CodeEditKit/NonUIExtensions/LSP/LanguageExtension.swift @@ -0,0 +1,150 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 27/03/2023. +// + +import Foundation +import LanguageClient +import Combine +import OSLog +import ProcessServiceClient +import ConcurrencyPlus +import LanguageServerProtocol +import ProcessEnv +import JSONRPC + +protocol LanguageExtension { + var client: RemoteLanguageServer { get } + +} + +final class UnrestrictedProcessTransport { + private var readHandler: ReadHandler = { _ in } + private let process: HostedProcess + private let taskQueue = TaskQueue() + private var subscription: AnyCancellable? + private let logger = Logger(subsystem: "com.chimehq.ChimeKit", category: "UnrestrictedProcessTransport") + + init(process: HostedProcess) { + self.process = process + } + + func beginMonitoringProcess() async throws { + let task = taskQueue.addOperation { + self.subscription = try await self.process.processEventPublisher + .sink(receiveCompletion: { _ in + }, receiveValue: { [weak self] event in + switch event { + case .stdout(let data): + self?.readHandler(data) + case .stderr(let data): + let output = String(data: data, encoding: .utf8) ?? "" + + self?.logger.info("stderr: \(output, privacy: .public)") + default: + break + } + }) + } + + try await task.value + } +} + +extension UnrestrictedProcessTransport: DataTransport { + func write(_ data: Data) { + taskQueue.addOperation { + try await self.process.write(data) + } + } + + func setReaderHandler(_ handler: @escaping ReadHandler) { + self.readHandler = handler + } + + func close() { + subscription?.cancel() + } +} + +/// Provides an interface to a LSP language server hosted by an intermediary process. +public class RemoteLanguageServer { + private let wrappedServer: JSONRPCLanguageServer + private var subscription: AnyCancellable? + private let logger = Logger(subsystem: "com.chimehq.ChimeKit", category: "RemoteLanguageServer") + + private let process: HostedProcess + private let taskQueue = TaskQueue() + public var terminationHandler: (() -> Void)? + + init(named serviceName: String, parameters: Process.ExecutionParameters) throws { + self.process = HostedProcess(named: serviceName, parameters: parameters) + let transport = UnrestrictedProcessTransport(process: process) + self.wrappedServer = JSONRPCLanguageServer(dataTransport: transport) + + taskQueue.addOperation { + self.logger.debug("launching remote server") + + do { + try await self.process.launch() + + self.subscription = try await self.process.processEventPublisher + .sink(receiveCompletion: { _ in + + }, receiveValue: { event in + switch event { + case .terminated: + self.terminationHandler?() + default: + break + } + }) + + try await transport.beginMonitoringProcess() + } catch { + self.logger.error("failed to launch: \(String(describing: error), privacy: .public)") + } + } + } + + private func stopProcess() { + self.taskQueue.addOperation { + do { + try await self.process.terminate() + } catch { + self.logger.error("failed to terminate: \(String(describing: error), privacy: .public)") + } + } + } + + public var logMessages: Bool { + get { return wrappedServer.logMessages } + set { wrappedServer.logMessages = newValue } + } +} + +extension RemoteLanguageServer: LanguageServerProtocol.Server { + public func setHandlers(_ handlers: ServerHandlers, completionHandler: @escaping (ServerError?) -> Void) { + wrappedServer.setHandlers(handlers, completionHandler: completionHandler) + } + + public func sendNotification(_ notif: ClientNotification, completionHandler: @escaping (ServerError?) -> Void) { + taskQueue.addOperation { + self.wrappedServer.sendNotification(notif, completionHandler: completionHandler) + } + } + + public func sendRequest(_ request: ClientRequest, completionHandler: @escaping (ServerResult) -> Void) { + taskQueue.addOperation { + self.wrappedServer.sendRequest(request, completionHandler: { (result: ServerResult) in + if case .success = result, case .shutdown = request { + self.stopProcess() + } + + completionHandler(result) + }) + } + } +} diff --git a/Sources/CodeEditKit/Targets/Target.swift b/Sources/CodeEditKit/Targets/Target.swift deleted file mode 100644 index c41aebf..0000000 --- a/Sources/CodeEditKit/Targets/Target.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Target.swift -// -// -// Created by Pavel Kasila on 27.03.22. -// - -import Foundation - -/// This structure stores information about the target to be available in CodeEdit for running -public struct Target: Identifiable { - - /** - * Initializes a target with parameters - * - Parameter id: The unique identifier of the target set by the extension - * - Parameter displayName: The name of the target to be displayed in the UI - * - Parameter executable: The executable to launch inside the pseudo terminal - * - Parameter args: an array of strings that is passed as the arguments to the underlying process - * - Parameter environment: an array of environment variables to pass to the child process. - * - Parameter execName: If provided, this is used as the Unix argv[0] parameter, otherwise, - * the executable is used as the args [0], this is used when the intent is to set a different process name - * than the file that backs it. - */ - public init(id: String, displayName: String, - executable: String, args: [String] = [], - environment: [String]? = nil, execName: String? = nil) { - self.id = id - self.displayName = displayName - self.executable = executable - self.args = args - self.environment = environment - self.execName = execName - } - - /// `id` is a unique identifier of the target set by the extension - public var id: String - - /// `displayName` is a name to be displayed in the UI to represent target - public var displayName: String - - /// `executable` is the executable to launch inside the pseudo terminal - public var executable: String - - /// `args` is an array of strings that is passed as the arguments to the underlying process - public var args: [String] = [] - - /// `environment` is an array of environment variables to pass to the child process. - public var environment: [String]? - - /// `execName` If provided, this is used as the Unix argv[0] parameter, otherwise, - /// the executable is used as the args [0], this is used when the intent is to set a different - /// process name than the file that backs it. - public var execName: String? -} diff --git a/Sources/CodeEditKit/TargetsAPI.swift b/Sources/CodeEditKit/TargetsAPI.swift deleted file mode 100644 index 4de0897..0000000 --- a/Sources/CodeEditKit/TargetsAPI.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// TargetsAPI.swift -// -// -// Created by Pavel Kasila on 27.03.22. -// - -import Foundation - -/// API for targets -public protocol TargetsAPI { - - /// Adds new target to the list - /// - Parameter target: the target to be added to the list - func add(target: Target) - - /// Deletes a target from the list - /// - Parameter target: the target to be removed from the list - func delete(target: Target) - - /// Clears all targets from the list - func clear() - -} diff --git a/Sources/CodeEditKit/UIExtensions/Environment/CEEnvironment.swift b/Sources/CodeEditKit/UIExtensions/Environment/CEEnvironment.swift new file mode 100644 index 0000000..bfdb401 --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Environment/CEEnvironment.swift @@ -0,0 +1,60 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 30/12/2022. +// + +import Foundation +import AnyCodable +import SwiftUI + +public struct _CEEnvironment: Codable, Equatable { + + public var test = false + public var complexValue: [String] = [] + + var otherKeys: [String: AnyCodable] = [:] + + mutating func update(_ keyPath: WritableKeyPath, _ value: Value) { + self[keyPath: keyPath] = value + } + + public subscript(key: K.Type) -> K.Value where K: CEEnvironmentKey { + get { + guard let data = otherKeys[key.identifier] else { return key.defaultValue } + return data.value as! K.Value + } + set { otherKeys[key.identifier] = AnyCodable(newValue) } + } +} + +@propertyWrapper +public struct CEEnvironment: DynamicProperty { + @Environment(\._ceEnvironment) var environment + + public init(_ keyPath: KeyPath<_CEEnvironment, Value>) { + self.keyPath = keyPath + } + + let keyPath: KeyPath<_CEEnvironment, Value> + + public var wrappedValue: Value { + environment[keyPath: keyPath] + } +} + +public struct _CEEnvironmentKey: EnvironmentKey { + public static let defaultValue = _CEEnvironment() +} + +public extension EnvironmentValues { + var _ceEnvironment: _CEEnvironmentKey.Value { + get { + return self[_CEEnvironmentKey.self] + } + set { + self[_CEEnvironmentKey.self] = newValue + } + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Environment/CEEnvironmentKey.swift b/Sources/CodeEditKit/UIExtensions/Environment/CEEnvironmentKey.swift new file mode 100644 index 0000000..3aefc2e --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Environment/CEEnvironmentKey.swift @@ -0,0 +1,26 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 30/12/2022. +// + +import Foundation + +public protocol CEEnvironmentKey { + + /// The associated type representing the type of the environment key's + /// value. + associatedtype Value: Codable + + static var identifier: String { get } + + /// The default value for the environment key. + static var defaultValue: Self.Value { get } +} + +public extension CEEnvironmentKey { + static var identifier: String { + String(describing: Self.self) + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Environment/CEEnvironmentModifier.swift b/Sources/CodeEditKit/UIExtensions/Environment/CEEnvironmentModifier.swift new file mode 100644 index 0000000..dc117e2 --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Environment/CEEnvironmentModifier.swift @@ -0,0 +1,26 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 02/01/2023. +// + +import SwiftUI + +public struct CEEnvironmentModifier: ViewModifier { + var keyPath: WritableKeyPath<_CEEnvironment, Value> + var value: Value + + public func body(content: Content) -> some View { + content + .transformEnvironment(\._ceEnvironment) { env in + env.update(keyPath, value) + } + } +} + +public extension View { + func ceEnvironment(_ keyPath: WritableKeyPath<_CEEnvironment, Value>, _ value: Value) -> some View { + return modifier(CEEnvironmentModifier(keyPath: keyPath, value: value)) + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Environment/CEOpenWindowEnvKey.swift b/Sources/CodeEditKit/UIExtensions/Environment/CEOpenWindowEnvKey.swift new file mode 100644 index 0000000..ae06adf --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Environment/CEOpenWindowEnvKey.swift @@ -0,0 +1,20 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 02/01/2023. +// + +import Foundation +import SwiftUI + +public struct CEOpenWindowEnvKey: EnvironmentKey { + public static var defaultValue: (String) async throws -> Void = { _ in } +} + +public extension EnvironmentValues { + var ceOpenWindow: CEOpenWindowEnvKey.Value { + get { self[CEOpenWindowEnvKey.self] } + set { self[CEOpenWindowEnvKey.self] = newValue } + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Environment/EnvironmentObjcPublisher.swift b/Sources/CodeEditKit/UIExtensions/Environment/EnvironmentObjcPublisher.swift new file mode 100644 index 0000000..e4d785c --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Environment/EnvironmentObjcPublisher.swift @@ -0,0 +1,36 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 30/12/2022. +// + +import Foundation +import ConcurrencyPlus + +@objc public protocol EnvironmentPublisherObjc { + func publishEnvironment(data: Data) +} + +public class EnvironmentPublisher: ObservableObject, EnvironmentPublisherObjc { + + @Published var environment = _CEEnvironment() + + var connection: NSXPCConnection? + + /// Send callbacks from functions + func update(@Encoded callback: Callbacks) async throws { + guard let $callback else { return } + try await connection!.withService { (service: EnvironmentPublisherObjc) in + service.publishEnvironment(data: $callback) + } + } + + public func publishEnvironment(data: Data) { + @Decoded<_CEEnvironment> var data = data + guard let $data else { return } + environment = $data + + print("update: received data \($data)") + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Settings/GeneralSettingsScene.swift b/Sources/CodeEditKit/UIExtensions/Settings/GeneralSettingsScene.swift new file mode 100644 index 0000000..a4f00f0 --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Settings/GeneralSettingsScene.swift @@ -0,0 +1,40 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 30/12/2022. +// + +import Foundation +import SwiftUI +import ExtensionKit + +struct GeneralSettingsScene: AppExtensionScene { + + @ViewBuilder + let content: () -> Content + + init(content: @escaping () -> Content) { + self.content = content + } + + var connection: NSXPCConnection? + var environmentWrapper = EnvironmentPublisher() + + var body: some AppExtensionScene { + PrimitiveAppExtensionScene(id: "Settings") { + GeneralSettingsView(environmentWrapper: environmentWrapper) { + Form { + content() + } + .formStyle(.grouped) + } + } onConnection: { connection in + connection.exportedInterface = .init(with: EnvironmentPublisherObjc.self) + connection.exportedObject = environmentWrapper + connection.resume() + + return true + } + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Settings/GeneralSettingsView.swift b/Sources/CodeEditKit/UIExtensions/Settings/GeneralSettingsView.swift new file mode 100644 index 0000000..4076f64 --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Settings/GeneralSettingsView.swift @@ -0,0 +1,20 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 30/12/2022. +// + +import Foundation +import SwiftUI + +struct GeneralSettingsView: View { + + @StateObject var environmentWrapper: EnvironmentPublisher + @ViewBuilder var content: Content + + var body: some View { + content + .environment(\._ceEnvironment, environmentWrapper.environment) + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Settings/SettingsExtension.swift b/Sources/CodeEditKit/UIExtensions/Settings/SettingsExtension.swift new file mode 100644 index 0000000..9e7e944 --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Settings/SettingsExtension.swift @@ -0,0 +1,29 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 30/12/2022. +// + +import Foundation +import SwiftUI +import ExtensionKit + +public protocol SettingsExtension: ObservableObject { + associatedtype SettingsBody: View + + @ViewBuilder + var settings: SettingsBody { get } +} + +public extension SettingsExtension { + var settingsScene: some AppExtensionScene { + GenericScene(sceneID: "Settings") { + Form { + self.settings + .environmentObject(self) + } + .formStyle(.grouped) + } + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Sidebar/Inspector.swift b/Sources/CodeEditKit/UIExtensions/Sidebar/Inspector.swift new file mode 100644 index 0000000..112832f --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Sidebar/Inspector.swift @@ -0,0 +1,38 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 25/03/2023. +// + +import SwiftUI +import ExtensionKit + +public struct Inspector: Sidebar { + public var body: Never { + fatalError() + } + + var id: String + + public init(id: String, content: () -> Content) { + self.id = id + self.content = content() + } + + @ViewBuilder var content: Content + + @_spi(CodeEdit) + public func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { + let scene = PrimitiveAppExtensionScene(id: id) { + VStack(alignment: .leading) { + content + .scrollContentBackground(.hidden) + .environmentObject(environment) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + let store = ResolvedSidebar.SidebarStore(sceneID: id, kind: .inspector) + return [ResolvedSidebar(scene: scene, store: store)] + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Sidebar/Navigator.swift b/Sources/CodeEditKit/UIExtensions/Sidebar/Navigator.swift new file mode 100644 index 0000000..cd3d8e4 --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Sidebar/Navigator.swift @@ -0,0 +1,38 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 25/03/2023. +// + +import SwiftUI +import ExtensionKit + +public struct Navigator: Sidebar { + public var body: Never { + fatalError() + } + + var id: String + + public init(id: String, content: () -> Content) { + self.id = id + self.content = content() + } + + @ViewBuilder var content: Content + + @_spi(CodeEdit) + public func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { + let scene = PrimitiveAppExtensionScene(id: id) { + VStack(alignment: .leading) { + content + .scrollContentBackground(.hidden) + .environmentObject(environment) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + let store = ResolvedSidebar.SidebarStore(sceneID: id, kind: .navigator) + return [ResolvedSidebar(scene: scene, store: store)] + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Sidebar/ResolvedSidebar.swift b/Sources/CodeEditKit/UIExtensions/Sidebar/ResolvedSidebar.swift new file mode 100644 index 0000000..0d4f9db --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Sidebar/ResolvedSidebar.swift @@ -0,0 +1,31 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 24/03/2023. +// + +import Foundation +import ExtensionKit + +public struct ResolvedSidebar { + + public enum Kind: Codable, Hashable { + case navigator, inspector + } + + var scene: PrimitiveAppExtensionScene + + var store: SidebarStore + + public struct SidebarStore: Codable, Hashable { + public var sceneID: String + public var icon: String? + public var help: String? + public var kind: Kind + } + + var extensionKind: ExtensionKind { + .sidebarItem(data: store) + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Sidebar/Sidebar+Help.swift b/Sources/CodeEditKit/UIExtensions/Sidebar/Sidebar+Help.swift new file mode 100644 index 0000000..f9fa287 --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Sidebar/Sidebar+Help.swift @@ -0,0 +1,61 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 24/03/2023. +// + +import SwiftUI +import ExtensionKit + +struct SidebarHelpModifier: Sidebar { + var help: String + var content: Content + + var body: Never { + fatalError() + } + + func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { + var sidebars = self.content.resolve(environment: environment) + for index in sidebars.indices { + sidebars[index].store.help = help + } + return sidebars + } +} + +struct SidebarIconModifier: Sidebar { + var icon: String + var content: Content + + var body: Never { + fatalError() + } + + func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { + var sidebars = self.content.resolve(environment: environment) + for index in sidebars.indices { + sidebars[index].store.icon = icon + } + return sidebars + } +} + +public extension Sidebar { + func help(_ message: String) -> some Sidebar { + SidebarHelpModifier(help: message, content: self) + } + + func icon(_ systemName: String) -> some Sidebar { + SidebarIconModifier(icon: systemName, content: self) + } + + func title(_ title: String) -> some Sidebar { + self + } + + func description(_ message: String) -> some Sidebar { + self + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Sidebar/Sidebar.swift b/Sources/CodeEditKit/UIExtensions/Sidebar/Sidebar.swift new file mode 100644 index 0000000..1d5bad3 --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Sidebar/Sidebar.swift @@ -0,0 +1,25 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 24/03/2023. +// + +import Foundation +import ExtensionKit + +public protocol Sidebar { + associatedtype Body: Sidebar + + @SidebarBuilder + var body: Body { get } + + @_spi(CodeEdit) + func resolve(environment: some ObservableObject) -> [ResolvedSidebar] +} + +public extension Sidebar { + func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { + body.resolve(environment: environment) + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Sidebar/SidebarBuilder.swift b/Sources/CodeEditKit/UIExtensions/Sidebar/SidebarBuilder.swift new file mode 100644 index 0000000..c717f87 --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Sidebar/SidebarBuilder.swift @@ -0,0 +1,21 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 24/03/2023. +// + +import Foundation + +@resultBuilder +public struct SidebarBuilder { + public static func buildPartialBlock(first: Never) -> Never {} + + public static func buildPartialBlock(first: some Sidebar) -> some Sidebar { + first + } + + public static func buildPartialBlock(accumulated: some Sidebar, next: some Sidebar) -> some Sidebar { + TupleSidebar(accumulated, next) + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Sidebar/SidebarExtension.swift b/Sources/CodeEditKit/UIExtensions/Sidebar/SidebarExtension.swift new file mode 100644 index 0000000..58a1372 --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Sidebar/SidebarExtension.swift @@ -0,0 +1,28 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 30/12/2022. +// + +import Foundation +import ExtensionKit +import SwiftUI + +public protocol SidebarExtension: ObservableObject { + + associatedtype SidebarBody: Sidebar + + @SidebarBuilder + var sidebars: SidebarBody { get } +} + +public extension SidebarExtension { + var sidebarScenes: some AppExtensionScene { + sidebars.resolve(environment: self).map(\.scene) + } + + var availableExtensions: [ExtensionKind] { + sidebars.resolve(environment: self).map(\.extensionKind) + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Sidebar/SidebarNever.swift b/Sources/CodeEditKit/UIExtensions/Sidebar/SidebarNever.swift new file mode 100644 index 0000000..87d872c --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Sidebar/SidebarNever.swift @@ -0,0 +1,20 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 24/03/2023. +// + +import Foundation + +extension Swift.Never: Sidebar { + + public var body: Swift.Never { + fatalError() + } + + @_spi(CodeEdit) + public func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { + fatalError() + } +} diff --git a/Sources/CodeEditKit/UIExtensions/Sidebar/TupleSidebar.swift b/Sources/CodeEditKit/UIExtensions/Sidebar/TupleSidebar.swift new file mode 100644 index 0000000..e09053e --- /dev/null +++ b/Sources/CodeEditKit/UIExtensions/Sidebar/TupleSidebar.swift @@ -0,0 +1,26 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 24/03/2023. +// + +import Foundation + +struct TupleSidebar: Sidebar { + var c0: C0 + var c1: C1 + + init(_ c0: C0, _ c1: C1) { + self.c0 = c0 + self.c1 = c1 + } + + var body: Never { + fatalError() + } + + func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { + c0.resolve(environment: environment) + c1.resolve(environment: environment) + } +} diff --git a/Sources/CodeEditKit/XPCWrapper.swift b/Sources/CodeEditKit/XPCWrapper.swift new file mode 100644 index 0000000..af7c310 --- /dev/null +++ b/Sources/CodeEditKit/XPCWrapper.swift @@ -0,0 +1,71 @@ +// +// File.swift +// +// +// Created by Wouter Hennen on 30/12/2022. +// + +import Foundation + +public typealias XPCReply = (Data?, Error?) -> Void + +public enum XPCError: Error { + case extensionDoesNotExist(description: String) + case identifierDoesNotExist(description: String) +} + +@objc +public protocol XPCWrappable { + func getExtensionURL(reply: @escaping XPCReply) + + func getExtensionKinds(reply: @escaping XPCReply) + + func getExtensionProcessIdentifier(reply: @escaping (Int32) -> Void) + + func doAction(with identifier: String, reply: @escaping XPCReply) + + func isDebug(reply: @escaping (Bool) -> Void) +} + +class XPCWrapper: XPCWrappable { + + var ext: any CodeEditExtension + + init(_ ext: any CodeEditExtension) { + self.ext = ext + } + + func getExtensionURL(reply: @escaping XPCReply) { + do { + let encoded = try JSONEncoder().encode(ext.extensionURL) + reply(encoded, nil) + } catch { + reply(nil, error) + } + } + + func getExtensionKinds(reply: @escaping XPCReply) { + do { + let encoded = try JSONEncoder().encode(ext.gatherAvailableExtensions()) + reply(encoded, nil) + } catch { + reply(nil, error) + } + } + + func getExtensionProcessIdentifier(reply: @escaping (Int32) -> Void) { + reply(ProcessInfo.processInfo.processIdentifier) + } + + func doAction(with identifier: String, reply: @escaping XPCReply) { + + } + + func isDebug(reply: @escaping (Bool) -> Void) { +#if DEBUG + reply(true) +#else + reply(false) +#endif + } +}