├── .gitattributes ├── .github ├── CodeEditKit-Icon-128@2x.png ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── pull_request_template.md ├── scripts │ ├── build-docc.sh │ └── tests.sh └── workflows │ ├── CI-pull-request.yml │ ├── CI-push.yml │ ├── add-to-project.yml │ ├── build-documentation.yml │ ├── swiftlint.yml │ └── tests.yml ├── .gitignore ├── .swiftlint.yml ├── LICENSE.md ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── CodeEditKit │ ├── CodableColorArray.swift │ ├── CodableWrappers.swift │ ├── CodeEditExtension+Body.swift │ ├── CodeEditExtension.swift │ ├── Debugging │ └── Logging.swift │ ├── Documentation.docc │ ├── CodeEditExtension.md │ ├── Commands.md │ ├── CreatingProject.md │ ├── Documentation.md │ ├── GenericExtension.md │ ├── LanguageServers.md │ ├── Settings.md │ ├── Sidebars.md │ ├── Snippets.md │ ├── Themes.md │ └── ToolbarItems.md │ ├── Entitlements.swift │ ├── ExtensionKind.swift │ ├── GenericScene.swift │ ├── UIExtensions │ ├── Environment │ │ ├── CEEnvironment.swift │ │ ├── CEEnvironmentKey.swift │ │ ├── CEEnvironmentModifier.swift │ │ ├── CEOpenWindowEnvKey.swift │ │ └── EnvironmentObjcPublisher.swift │ ├── Settings │ │ ├── GeneralSettingsScene.swift │ │ ├── GeneralSettingsView.swift │ │ └── SettingsExtension.swift │ └── Sidebar │ │ ├── Inspector.swift │ │ ├── Navigator.swift │ │ ├── ResolvedSidebar.swift │ │ ├── Sidebar+Help.swift │ │ ├── Sidebar.swift │ │ ├── SidebarBuilder.swift │ │ ├── SidebarExtension.swift │ │ ├── SidebarNever.swift │ │ └── TupleSidebar.swift │ └── XPCWrapper.swift └── Tests └── CodeEditKitTests └── CEExtensionKitTests.swift /.gitattributes: -------------------------------------------------------------------------------- 1 | .github/** linguist-vendored 2 | Sources/CodeEditTextView/Documentation.docc/** linguist-documentation 3 | -------------------------------------------------------------------------------- /.github/CodeEditKit-Icon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeEditApp/CodeEditKit/ad28213a968586abb0cb21a8a56a3587227895f1/.github/CodeEditKit-Icon-128@2x.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Something is not working as expected. 3 | title: 🐞 4 | labels: bug 5 | 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Description 10 | placeholder: >- 11 | A clear and concise description of what the bug is... 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | attributes: 17 | label: To Reproduce 18 | description: >- 19 | Steps to reliably reproduce the behavior. 20 | placeholder: | 21 | 1. Go to '...' 22 | 2. Click on '....' 23 | 3. Scroll down to '....' 24 | 4. See error 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | attributes: 30 | label: Expected Behavior 31 | placeholder: >- 32 | A clear and concise description of what you expected to happen... 33 | validations: 34 | required: true 35 | 36 | - type: textarea 37 | attributes: 38 | label: Version Information 39 | description: >- 40 | click on the version number on the welcome screen 41 | value: | 42 | CodeEditKit: [e.g. 0.0.x-alpha.y] 43 | macOS: [e.g. 13.2.1] 44 | Xcode: [e.g. 14.2] 45 | 46 | - type: textarea 47 | attributes: 48 | label: Additional Context 49 | placeholder: >- 50 | Any other context or considerations about the bug... 51 | 52 | - type: textarea 53 | attributes: 54 | label: Screenshots 55 | placeholder: >- 56 | If applicable, please provide relevant screenshots or screen recordings... 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: ✨ Feature request 2 | description: Suggest an idea for this project 3 | title: ✨ 4 | labels: enhancement 5 | 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Description 10 | placeholder: >- 11 | A clear and concise description of what you would like to happen... 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | attributes: 17 | label: Alternatives Considered 18 | placeholder: >- 19 | Any alternative solutions or features you've considered... 20 | 21 | - type: textarea 22 | attributes: 23 | label: Additional Context 24 | placeholder: >- 25 | Any other context or considerations about the feature request... 26 | 27 | - type: textarea 28 | attributes: 29 | label: Screenshots 30 | placeholder: >- 31 | If applicable, please provide relevant screenshots or screen recordings... 32 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | 7 | ### Related Issues 8 | 9 | 10 | 11 | 12 | 13 | * #ISSUE_NUMBER 14 | 15 | ### Checklist 16 | 17 | 18 | 19 | - [ ] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) 20 | - [ ] The issues this PR addresses are related to each other 21 | - [ ] My changes generate no new warnings 22 | - [ ] My code builds and runs on my machine 23 | - [ ] My changes are all related to the related issue above 24 | - [ ] I documented my code 25 | 26 | ### Screenshots 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /.github/scripts/build-docc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export LC_CTYPE=en_US.UTF-8 4 | 5 | set -o pipefail && xcodebuild clean docbuild -scheme CodeEditKit \ 6 | -destination generic/platform=macos \ 7 | -skipPackagePluginValidation \ 8 | OTHER_DOCC_FLAGS="--transform-for-static-hosting --hosting-base-path CodeEditKit --output-path ./docs" | xcpretty 9 | -------------------------------------------------------------------------------- /.github/scripts/tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ARCH="" 4 | 5 | if [ $1 = "arm" ] 6 | then 7 | ARCH="arm64" 8 | else 9 | ARCH="x86_64" 10 | fi 11 | 12 | echo "Building with arch: ${ARCH}" 13 | 14 | export LC_CTYPE=en_US.UTF-8 15 | 16 | set -o pipefail && arch -"${ARCH}" xcodebuild \ 17 | -scheme CodeEditKit \ 18 | -destination "platform=macos,arch=${ARCH}" \ 19 | -skipPackagePluginValidation \ 20 | clean test | xcpretty 21 | -------------------------------------------------------------------------------- /.github/workflows/CI-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: CI - Pull Request 2 | on: 3 | pull_request: 4 | branches: 5 | - 'main' 6 | workflow_dispatch: 7 | jobs: 8 | swiftlint: 9 | name: SwiftLint 10 | uses: ./.github/workflows/swiftlint.yml 11 | secrets: inherit 12 | test: 13 | name: Testing CodeEditKit 14 | needs: swiftlint 15 | uses: ./.github/workflows/tests.yml 16 | secrets: inherit 17 | -------------------------------------------------------------------------------- /.github/workflows/CI-push.yml: -------------------------------------------------------------------------------- 1 | name: CI - Push to main 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | workflow_dispatch: 7 | jobs: 8 | swiftlint: 9 | name: SwiftLint 10 | uses: ./.github/workflows/swiftlint.yml 11 | secrets: inherit 12 | test: 13 | name: Testing CodeEditKit 14 | needs: swiftlint 15 | uses: ./.github/workflows/tests.yml 16 | secrets: inherit 17 | build_documentation: 18 | name: Build Documentation 19 | needs: [swiftlint, test] 20 | uses: ./.github/workflows/build-documentation.yml 21 | secrets: inherit 22 | -------------------------------------------------------------------------------- /.github/workflows/add-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Add new issues to project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-project: 10 | name: Add new issues labeled with enhancement or bug to project 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@v0.4.0 14 | with: 15 | # You can target a repository in a different organization 16 | # to the issue 17 | project-url: https://github.com/orgs/CodeEditApp/projects/3 18 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 19 | labeled: enhancement, bug 20 | label-operator: OR 21 | -------------------------------------------------------------------------------- /.github/workflows/build-documentation.yml: -------------------------------------------------------------------------------- 1 | name: build-documentation 2 | on: 3 | workflow_dispatch: 4 | workflow_call: 5 | jobs: 6 | build-docc: 7 | runs-on: [self-hosted, macOS] 8 | steps: 9 | - name: Checkout repository 10 | uses: actions/checkout@v3 11 | - name: Build Documentation 12 | run: exec ./.github/scripts/build-docc.sh 13 | - name: Init new repo in dist folder and commit generated files 14 | run: | 15 | cd docs 16 | git init 17 | git add -A 18 | git config --local user.email "action@github.com" 19 | git config --local user.name "GitHub Action" 20 | git commit -m 'deploy' 21 | 22 | - name: Force push to destination branch 23 | uses: ad-m/github-push-action@v0.6.0 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | branch: docs 27 | force: true 28 | directory: ./docs 29 | 30 | ############################ 31 | ##### IMPORTANT NOTICE ##### 32 | ############################ 33 | # This was used to build the documentation catalog until 34 | # it didn't produce the 'documentation' directory anymore. 35 | # 36 | # - uses: fwcd/swift-docc-action@v1.0.2 37 | # with: 38 | # target: CodeEditKit 39 | # output: ./docs 40 | # hosting-base-path: CodeEditKit 41 | # disable-indexing: 'true' 42 | # transform-for-static-hosting: 'true' 43 | # 44 | # The command that this plugin uses is: 45 | # 46 | # swift package --allow-writing-to-directory ./docs generate-documentation \ 47 | # --target CodeEditKit 48 | # --output-path ./docs 49 | # --hosting-base-path CodeEditKit 50 | # --disable-indexing 51 | # --transform-for-static-hosting 52 | # 53 | # We now use xcodebuild to build the documentation catalog instead. 54 | # 55 | -------------------------------------------------------------------------------- /.github/workflows/swiftlint.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | on: 3 | workflow_dispatch: 4 | workflow_call: 5 | jobs: 6 | SwiftLint: 7 | runs-on: [self-hosted, macOS] 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: GitHub Action for SwiftLint with --strict 11 | run: swiftlint --strict 12 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | workflow_dispatch: 4 | workflow_call: 5 | jobs: 6 | code-edit-kit-tests: 7 | name: Testing CodeEditKit 8 | runs-on: [self-hosted, macOS] 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v3 12 | - name: Testing Package 13 | run: exec ./.github/scripts/tests.sh arm 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .swiftpm 9 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - force_cast 3 | 4 | line_length: 160 5 | 6 | type_name: 7 | allowed_symbols: ['_'] 8 | excluded: 9 | - ID 10 | 11 | identifier_name: 12 | min_length: 2 13 | allowed_symbols: ['$', '_'] 14 | excluded: 15 | - id 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 CodeEdit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "anycodable", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/Flight-School/AnyCodable", 7 | "state" : { 8 | "revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05", 9 | "version" : "0.6.7" 10 | } 11 | }, 12 | { 13 | "identity" : "concurrencyplus", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/ChimeHQ/ConcurrencyPlus", 16 | "state" : { 17 | "revision" : "8dc56499412a373d617d50d059116bccf44b9874", 18 | "version" : "0.4.2" 19 | } 20 | }, 21 | { 22 | "identity" : "swiftlintplugin", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/lukepistrol/SwiftLintPlugin", 25 | "state" : { 26 | "revision" : "ea6d3ca895b49910f790e98e4b4ca658e0fe490e", 27 | "version" : "0.54.0" 28 | } 29 | } 30 | ], 31 | "version" : 2 32 | } 33 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "CodeEditKit", 7 | platforms: [ 8 | .macOS(.v13) 9 | ], 10 | products: [ 11 | .library( 12 | name: "CodeEditKit", 13 | type: .dynamic, 14 | targets: ["CodeEditKit"]) 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/ChimeHQ/ConcurrencyPlus", from: "0.4.1"), 18 | .package( 19 | url: "https://github.com/lukepistrol/SwiftLintPlugin", 20 | from: "0.2.2" 21 | ), 22 | .package( 23 | url: "https://github.com/Flight-School/AnyCodable", 24 | from: "0.6.0" 25 | ) 26 | ], 27 | targets: [ 28 | .target( 29 | name: "CodeEditKit", 30 | dependencies: ["AnyCodable", "ConcurrencyPlus"], 31 | plugins: [.plugin(name: "SwiftLint", package: "SwiftLintPlugin")] 32 | ), 33 | .testTarget( 34 | name: "CodeEditKitTests", 35 | dependencies: ["CodeEditKit"]) 36 | ] 37 | ) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

CodeEditKit

4 |

5 | 6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 |

14 | 15 | CodeEditKit is a dynamic library which is shared between CodeEdit and its extensions. It allows them to communicate with and understand one another. 16 | 17 | codeeditkit-flow 18 | 19 | ![Github Tests](https://img.shields.io/github/actions/workflow/status/CodeEditApp/CodeEditKit/tests.yml?branch=main&label=tests&style=flat-square) 20 | ![Documentation](https://img.shields.io/github/actions/workflow/status/CodeEditApp/CodeEditKit/build-documentation.yml?branch=main&label=docs&style=flat-square) 21 | ![GitHub Repo stars](https://img.shields.io/github/stars/CodeEditApp/CodeEditKit?style=flat-square) 22 | ![GitHub forks](https://img.shields.io/github/forks/CodeEditApp/CodeEditKit?style=flat-square) 23 | [![Discord Badge](https://img.shields.io/discord/951544472238444645?color=5865F2&label=Discord&logo=discord&logoColor=white&style=flat-square)](https://discord.gg/vChUXVf9Em) 24 | 25 | ## Documentation 26 | 27 | The documentation for `CodeEditKit` can be viewed [here](https://codeeditapp.github.io/CodeEditKit/documentation/codeeditkit/). 28 | 29 | ## License 30 | 31 | Licensed under the [MIT license](https://github.com/CodeEditApp/CodeEdit/blob/main/LICENSE.md). 32 | 33 | ## Related Repositories 34 | 35 | 36 | 37 | 43 | 49 | 55 | 61 | 67 | 68 |
38 | 39 | 40 |

        CodeEdit        

41 |
42 |
44 | 45 | 46 |

CodeEditSourceEditor

47 |
48 |
50 | 51 | 52 |

CodeEditTextView

53 |
54 |
56 | 57 | 58 |

CodeEditLanguages

59 |
60 |
62 | 63 | 64 |

CodeEditCLI

65 |
66 |
69 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/CodableColorArray.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 30/12/2022. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | @propertyWrapper 12 | public struct CodableColorArray { 13 | public var wrappedValue: [NSColor] 14 | 15 | public init(wrappedValue: [NSColor]) { 16 | self.wrappedValue = wrappedValue 17 | } 18 | } 19 | 20 | extension CodableColorArray: Codable { 21 | public init(from decoder: Decoder) throws { 22 | let container = try decoder.singleValueContainer() 23 | let data = try container.decode(Data.self) 24 | 25 | guard let color = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: NSColor.self, from: data) else { 26 | throw DecodingError.dataCorruptedError( 27 | in: container, 28 | debugDescription: "Invalid color" 29 | ) 30 | } 31 | wrappedValue = color 32 | } 33 | 34 | public func encode(to encoder: Encoder) throws { 35 | var container = encoder.singleValueContainer() 36 | let data = try NSKeyedArchiver.archivedData(withRootObject: wrappedValue, requiringSecureCoding: true) 37 | try container.encode(data) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/CodableWrappers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File 2.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 06/12/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | @propertyWrapper 11 | public struct Encoded { 12 | public var wrappedValue: T 13 | 14 | public init(wrappedValue: T) { 15 | self.wrappedValue = wrappedValue 16 | } 17 | 18 | public var errorDescription: String? { 19 | do { 20 | _ = try JSONEncoder().encode(wrappedValue) 21 | return nil 22 | } catch { 23 | return error.localizedDescription 24 | } 25 | } 26 | 27 | public var projectedValue: Data? { 28 | try? JSONEncoder().encode(wrappedValue) 29 | } 30 | } 31 | 32 | @propertyWrapper 33 | public struct Decoded { 34 | public var wrappedValue: Data 35 | 36 | public init(wrappedValue: Data) { 37 | self.wrappedValue = wrappedValue 38 | } 39 | 40 | public var errorDescription: String? { 41 | do { 42 | _ = try JSONDecoder().decode(T.self, from: wrappedValue) 43 | return nil 44 | } catch { 45 | return error.localizedDescription 46 | } 47 | } 48 | 49 | public var projectedValue: T? { 50 | try? JSONDecoder().decode(T.self, from: wrappedValue) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/CodeEditExtension+Body.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 03/01/2023. 6 | // 7 | 8 | import Foundation 9 | import ExtensionKit 10 | 11 | struct EmptyAppExtensionScene: AppExtensionScene { 12 | var body: Never { 13 | fatalError() 14 | } 15 | } 16 | 17 | public extension CodeEditExtension { 18 | var body: some AppExtensionScene { 19 | EmptyAppExtensionScene() 20 | } 21 | } 22 | 23 | public extension CodeEditExtension where Self: SettingsExtension { 24 | var body: some AppExtensionScene { 25 | settingsScene 26 | } 27 | } 28 | 29 | public extension CodeEditExtension where Self: SidebarExtension { 30 | var body: some AppExtensionScene { 31 | sidebarScenes 32 | } 33 | } 34 | 35 | public extension CodeEditExtension where Self: SettingsExtension & SidebarExtension { 36 | @AppExtensionSceneBuilder 37 | var body: some AppExtensionScene { 38 | settingsScene 39 | sidebarScenes 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/CodeEditExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 30/12/2022. 6 | // 7 | 8 | import ExtensionKit 9 | import ExtensionFoundation 10 | 11 | public protocol CodeEditExtension: AppExtension { 12 | 13 | /// UI scenes of the extension. 14 | associatedtype Body: AppExtensionScene 15 | 16 | /// Extension Configuration. 17 | associatedtype Configuration = AppExtensionSceneConfiguration 18 | 19 | /// A brief description of the extension. Should be max a few words. 20 | var description: String { get } 21 | 22 | /// A list of Entitlements the app needs, e.g. Network Access. 23 | var entitlements: [Entitlement] { get } 24 | 25 | /// UI scenes of the extension. 26 | /// Use the default implementation. 27 | var body: Body { get } 28 | } 29 | 30 | extension CodeEditExtension { 31 | 32 | var extensionURL: URL { 33 | Bundle.main.bundleURL 34 | } 35 | 36 | func gatherAvailableExtensions() -> [ExtensionKind] { 37 | var extensions: [ExtensionKind] = [] 38 | 39 | if self is any SettingsExtension { 40 | extensions.append(.settings) 41 | } 42 | 43 | if let self = self as? any SidebarExtension { 44 | extensions.append(contentsOf: self.availableExtensions) 45 | } 46 | 47 | return extensions 48 | } 49 | } 50 | 51 | public extension CodeEditExtension where Self: AnyObject { 52 | /// XPC Configuration for communication with CodeEdit. 53 | /// Use the default implementation. 54 | var configuration: AppExtensionSceneConfiguration { 55 | AppExtensionSceneConfiguration(self.body, configuration: SettingsExtensionConfiguration(self)) 56 | } 57 | } 58 | 59 | struct SettingsExtensionConfiguration: AppExtensionConfiguration { 60 | public func accept(connection: NSXPCConnection) -> Bool { 61 | guard let appExtension else { 62 | return false 63 | } 64 | 65 | connection.exportedInterface = .init(with: XPCWrappable.self) 66 | connection.exportedObject = XPCWrapper(appExtension) 67 | connection.resume() 68 | 69 | return true 70 | } 71 | 72 | weak var appExtension: E? 73 | 74 | public init(_ appExtension: E) { 75 | self.appExtension = appExtension 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/Debugging/Logging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 22/05/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public func print(_ items: Any..., separator: String = " ", terminator: String = "\n") { 11 | let formedString = items.map { String(describing: $0) }.joined(separator: separator) 12 | NSLog(formedString) 13 | } 14 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/Documentation.docc/CodeEditExtension.md: -------------------------------------------------------------------------------- 1 | # ``CodeEditKit/CodeEditExtension`` 2 | 3 | @Metadata { 4 | @DocumentationExtension(mergeBehavior: append) 5 | } 6 | 7 | The base extension. 8 | 9 | ## Overview 10 | 11 | All extensions supported by CodeEdit are defined as a ``CodeEditExtension``. 12 | This protocol provides basic information about the extension, such as the ``description``, name and ``entitlements``. 13 | 14 | This type is the entrypoint of your extension, so it should be marked with `@main`. 15 | 16 | Additional extensions can be defined by extending the class that implements ``CodeEditExtension``. 17 | 18 | ## Example 19 | 20 | ```swift 21 | @main 22 | final class ExampleExtension: CodeEditExtension { 23 | var description: String = "test" 24 | 25 | var entitlements: [Entitlement] = [.currentfile] 26 | } 27 | 28 | // Add a Settings Pane 29 | extension ExampleExtension: SettingsExtension { 30 | var settings: some View { 31 | SettingsView() 32 | } 33 | } 34 | 35 | // The Settings View 36 | struct SettingsView: View { 37 | @State var enabled = false 38 | var body: some View { 39 | Toggle("Enabled", isOn: $enabled) 40 | } 41 | } 42 | ``` 43 | 44 | > 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. 45 | > ```swift 46 | > struct AView: View { 47 | > @EnvironmentObject var main: ExampleExtension 48 | > } 49 | > ``` 50 | 51 | 52 | 53 | ## Topics 54 | 55 | ### Group 56 | 57 | - ``Symbol`` 58 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/Documentation.docc/Commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | Quick actions 4 | 5 | ## Overview 6 | 7 | ## Topics 8 | 9 | ### Group 10 | 11 | - ``Symbol`` 12 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/Documentation.docc/CreatingProject.md: -------------------------------------------------------------------------------- 1 | # Project Setup 2 | 3 | ## Overview 4 | 5 | To start developing extensions, you first need to configure your project. 6 | 7 | ## Steps 8 | 9 | This is a WIP, and should be explained better. 10 | 11 | 1. Create a new Xcode project. As the template, choose "App" (make sure you select the macOS tab, not the multiplatform tab) 12 | 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. 13 | 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) 14 | 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. 15 | 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. 16 | 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``. 17 | 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. 18 | 19 | ## Topics 20 | 21 | ### Group 22 | 23 | - ``Symbol`` 24 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``CodeEditKit`` 2 | 3 | CodeEditKit is a dynamic library which is shared between CodeEdit and it's extensions. It allows them to communicate with and understand one another. 4 | 5 | ## Overview 6 | 7 | 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. 8 | 9 | CodeEditKit aims to provide an easy and straightforward API to implement these extensions, with modern technologies like Swift, SwiftUI and ExtensionKit. 10 | 11 | ### Extension Types 12 | 13 | There are lots of extension types you can use to extend the functionality of CodeEdit. 14 | 15 | #### General Extensions 16 | - (WIP) 17 | - (Not Started) 18 | 19 | #### Language Extensions 20 | - (Not Started) 21 | - (WIP) 22 | 23 | #### UI Extensions 24 | - ``SidebarExtension`` (Beta) 25 | - (WIP) 26 | - (Beta) 27 | 28 | ### Getting Started 29 | To setup your Xcode project, have a look at the section. 30 | 31 | Next, have a look at ``CodeEditExtension``. This is the base protocol that will define the main structure of your extension. 32 | All extensions that you'll add will extend (:p) this type. 33 | 34 | Finally, try adding an extension to your newly created type. A good first recommendation is the extension, as you'll likely need this later. 35 | 36 | ### Development & Debugging 37 | Developing extensions with ExtensionKit has a few annoyances: 38 | - Prints do not appear in the Xcode console. Instead, the debug information of CodeEdit (not the extension) is printed. 39 | - Modifying extension requires CodeEdit reload. 40 | - An extension needs to be sandboxed. 41 | 42 | CodeEdit tries to overcome these issues by offering a few solutions. 43 | 44 | 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. 45 | 46 | Additionally, if you choose to use the powerful Logger system to debug, CodeEdit provides support for various log types and will color them accordingly. 47 | 48 | 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. 49 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/Documentation.docc/GenericExtension.md: -------------------------------------------------------------------------------- 1 | # ``CodeEditKit/GenericExtension`` 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | Text 8 | 9 | > 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. 10 | 11 | ## Topics 12 | 13 | ### Group 14 | 15 | - ``Symbol`` 16 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/Documentation.docc/LanguageServers.md: -------------------------------------------------------------------------------- 1 | # Language Servers 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | Text 8 | 9 | ## Topics 10 | 11 | ### Group 12 | 13 | - ``Symbol`` 14 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/Documentation.docc/Settings.md: -------------------------------------------------------------------------------- 1 | # Settings 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | The ``SettingsExtension`` allows an extension to provide additional settings to the settings window of CodeEdit. 8 | Extension-specific settings can be configured this way. 9 | 10 | ``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. 11 | 12 | ## Example 13 | 14 | ```swift 15 | @main 16 | final class SettingsExampleExtension: CodeEditExtension { 17 | 18 | var description: String = "" 19 | 20 | var entitlements: [Entitlement] = [.currentfile] 21 | 22 | } 23 | 24 | extension SettingsExampleExtension: SettingsExtension { 25 | var settings: some View { 26 | SettingsView() 27 | } 28 | } 29 | 30 | struct SettingsView: View { 31 | @AppStorage("appstoragekey") var appstoragekey = false 32 | 33 | var body: some View { 34 | Toggle("App Storage Key", isOn: $appstoragekey) 35 | } 36 | } 37 | ``` 38 | 39 | ## Topics 40 | 41 | ### Group 42 | 43 | - ``Symbol`` 44 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/Documentation.docc/Sidebars.md: -------------------------------------------------------------------------------- 1 | # ``CodeEditKit/SidebarExtension`` 2 | 3 | @Metadata { 4 | @DocumentationExtension(mergeBehavior: append) 5 | } 6 | 7 | Navigator & Inspector UI Extensions 8 | 9 | ## Overview 10 | 11 | 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. 12 | 13 | ## Example 14 | 15 | ```swift 16 | @main 17 | final class SidebarExtensionExample: CodeEditExtension { 18 | var description: String = "" 19 | 20 | var entitlements: [Entitlement] = [] 21 | } 22 | 23 | extension SidebarExtensionExample: SidebarExtension { 24 | var sidebars: some Sidebar { 25 | Inspector(id: "umbrellaInspector") { 26 | Form { 27 | Text("Hello, world!") 28 | } 29 | .formStyle(.grouped) 30 | } 31 | .help("Umbrella Inspector") 32 | .icon("umbrella") 33 | 34 | Navigator(id: "carrotNavigator") { 35 | Form { 36 | Text("Hello, world!") 37 | } 38 | .formStyle(.grouped) 39 | } 40 | .help("Carrot Navigator") 41 | .icon("carrot") 42 | } 43 | } 44 | 45 | ``` 46 | 47 | ## Topics 48 | 49 | ### Sidebar Modifiers 50 | 51 | - ``Sidebar/help(_:)`` 52 | - ``Sidebar/icon(_:)`` 53 | - ``Sidebar/description(_:)`` 54 | - ``Sidebar/title(_:)`` 55 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/Documentation.docc/Snippets.md: -------------------------------------------------------------------------------- 1 | # Snippets 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | Text 8 | 9 | ## Topics 10 | 11 | ### Group 12 | 13 | - ``Symbol`` 14 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/Documentation.docc/Themes.md: -------------------------------------------------------------------------------- 1 | # Themes 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | Text 8 | 9 | ## Topics 10 | 11 | ### Group 12 | 13 | - ``Symbol`` 14 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/Documentation.docc/ToolbarItems.md: -------------------------------------------------------------------------------- 1 | # Toolbar Items 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | Text 8 | 9 | ## Topics 10 | 11 | ### Group 12 | 13 | - ``Symbol`` 14 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/Entitlements.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 30/12/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Entitlement: CustomStringConvertible, Codable, CaseIterable { 11 | case workspace 12 | case currentfile 13 | 14 | public var description: String { 15 | switch self { 16 | case .workspace: 17 | return "Workspace" 18 | case .currentfile: 19 | return "Current File" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/ExtensionKind.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 30/12/2022. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public enum ExtensionKind: Codable, Hashable, CustomStringConvertible { 11 | case sidebarItem(data: ResolvedSidebar.SidebarStore) 12 | case action(actionID: String) 13 | case theme(themeID: String) 14 | case settings 15 | 16 | public var description: String { 17 | switch self { 18 | case .sidebarItem(let data): 19 | return "Sidebar with ID \(data.sceneID)" 20 | case .action(let actionID): 21 | return "Action with ID \(actionID)" 22 | case .theme(let themeID): 23 | return "Theme with ID \(themeID)" 24 | case .settings: 25 | return "Settings" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/GenericScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 02/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | import ExtensionKit 10 | 11 | public enum Callbacks: Codable { 12 | case openWindow(id: String) 13 | } 14 | 15 | struct GenericScene: AppExtensionScene { 16 | 17 | var sceneID: String 18 | 19 | @ViewBuilder 20 | var content: Content 21 | 22 | var environmentWrapper = EnvironmentPublisher() 23 | 24 | var connection: NSXPCConnection? 25 | 26 | var body: some AppExtensionScene { 27 | PrimitiveAppExtensionScene(id: sceneID) { 28 | GeneralSettingsView(environmentWrapper: environmentWrapper) { 29 | content 30 | .environment(\.ceOpenWindow) { id in 31 | try await environmentWrapper.update(callback: .openWindow(id: id)) 32 | } 33 | } 34 | } onConnection: { connection in 35 | connection.exportedInterface = .init(with: EnvironmentPublisherObjc.self) 36 | connection.remoteObjectInterface = .init(with: EnvironmentPublisherObjc.self) 37 | connection.exportedObject = environmentWrapper 38 | environmentWrapper.connection = connection 39 | connection.resume() 40 | 41 | return true 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Environment/CEEnvironment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 30/12/2022. 6 | // 7 | 8 | import Foundation 9 | import AnyCodable 10 | import SwiftUI 11 | 12 | public struct _CEEnvironment: Codable, Equatable { 13 | 14 | public var test = false 15 | public var complexValue: [String] = [] 16 | 17 | var otherKeys: [String: AnyCodable] = [:] 18 | 19 | mutating func update(_ keyPath: WritableKeyPath, _ value: Value) { 20 | self[keyPath: keyPath] = value 21 | } 22 | 23 | public subscript(key: K.Type) -> K.Value where K: CEEnvironmentKey { 24 | get { 25 | guard let data = otherKeys[key.identifier] else { return key.defaultValue } 26 | return data.value as! K.Value 27 | } 28 | set { otherKeys[key.identifier] = AnyCodable(newValue) } 29 | } 30 | } 31 | 32 | @propertyWrapper 33 | public struct CEEnvironment: DynamicProperty { 34 | @Environment(\._ceEnvironment) var environment 35 | 36 | public init(_ keyPath: KeyPath<_CEEnvironment, Value>) { 37 | self.keyPath = keyPath 38 | } 39 | 40 | let keyPath: KeyPath<_CEEnvironment, Value> 41 | 42 | public var wrappedValue: Value { 43 | environment[keyPath: keyPath] 44 | } 45 | } 46 | 47 | public struct _CEEnvironmentKey: EnvironmentKey { 48 | public static let defaultValue = _CEEnvironment() 49 | } 50 | 51 | public extension EnvironmentValues { 52 | var _ceEnvironment: _CEEnvironmentKey.Value { 53 | get { 54 | return self[_CEEnvironmentKey.self] 55 | } 56 | set { 57 | self[_CEEnvironmentKey.self] = newValue 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Environment/CEEnvironmentKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 30/12/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol CEEnvironmentKey { 11 | 12 | /// The associated type representing the type of the environment key's 13 | /// value. 14 | associatedtype Value: Codable 15 | 16 | static var identifier: String { get } 17 | 18 | /// The default value for the environment key. 19 | static var defaultValue: Self.Value { get } 20 | } 21 | 22 | public extension CEEnvironmentKey { 23 | static var identifier: String { 24 | String(describing: Self.self) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Environment/CEEnvironmentModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 02/01/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | public struct CEEnvironmentModifier: ViewModifier { 11 | var keyPath: WritableKeyPath<_CEEnvironment, Value> 12 | var value: Value 13 | 14 | public func body(content: Content) -> some View { 15 | content 16 | .transformEnvironment(\._ceEnvironment) { env in 17 | env.update(keyPath, value) 18 | } 19 | } 20 | } 21 | 22 | public extension View { 23 | func ceEnvironment(_ keyPath: WritableKeyPath<_CEEnvironment, Value>, _ value: Value) -> some View { 24 | return modifier(CEEnvironmentModifier(keyPath: keyPath, value: value)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Environment/CEOpenWindowEnvKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 02/01/2023. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | public struct CEOpenWindowEnvKey: EnvironmentKey { 12 | public static var defaultValue: (String) async throws -> Void = { _ in } 13 | } 14 | 15 | public extension EnvironmentValues { 16 | var ceOpenWindow: CEOpenWindowEnvKey.Value { 17 | get { self[CEOpenWindowEnvKey.self] } 18 | set { self[CEOpenWindowEnvKey.self] = newValue } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Environment/EnvironmentObjcPublisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 30/12/2022. 6 | // 7 | 8 | import Foundation 9 | import ConcurrencyPlus 10 | 11 | @objc public protocol EnvironmentPublisherObjc { 12 | func publishEnvironment(data: Data) 13 | } 14 | 15 | public class EnvironmentPublisher: ObservableObject, EnvironmentPublisherObjc { 16 | 17 | @Published var environment = _CEEnvironment() 18 | 19 | var connection: NSXPCConnection? 20 | 21 | /// Send callbacks from functions 22 | func update(@Encoded callback: Callbacks) async throws { 23 | guard let $callback else { return } 24 | try await connection!.withService { (service: EnvironmentPublisherObjc) in 25 | service.publishEnvironment(data: $callback) 26 | } 27 | } 28 | 29 | public func publishEnvironment(data: Data) { 30 | @Decoded<_CEEnvironment> var data = data 31 | guard let $data else { return } 32 | environment = $data 33 | 34 | print("update: received data \($data)") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Settings/GeneralSettingsScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 30/12/2022. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import ExtensionKit 11 | 12 | struct GeneralSettingsScene: AppExtensionScene { 13 | 14 | @ViewBuilder 15 | let content: () -> Content 16 | 17 | init(content: @escaping () -> Content) { 18 | self.content = content 19 | } 20 | 21 | var connection: NSXPCConnection? 22 | var environmentWrapper = EnvironmentPublisher() 23 | 24 | var body: some AppExtensionScene { 25 | PrimitiveAppExtensionScene(id: "Settings") { 26 | GeneralSettingsView(environmentWrapper: environmentWrapper) { 27 | Form { 28 | content() 29 | } 30 | .formStyle(.grouped) 31 | } 32 | } onConnection: { connection in 33 | connection.exportedInterface = .init(with: EnvironmentPublisherObjc.self) 34 | connection.exportedObject = environmentWrapper 35 | connection.resume() 36 | 37 | return true 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Settings/GeneralSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 30/12/2022. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct GeneralSettingsView: View { 12 | 13 | @StateObject var environmentWrapper: EnvironmentPublisher 14 | @ViewBuilder var content: Content 15 | 16 | var body: some View { 17 | content 18 | .environment(\._ceEnvironment, environmentWrapper.environment) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Settings/SettingsExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 30/12/2022. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import ExtensionKit 11 | 12 | public protocol SettingsExtension: ObservableObject { 13 | associatedtype SettingsBody: View 14 | 15 | @ViewBuilder 16 | var settings: SettingsBody { get } 17 | } 18 | 19 | public extension SettingsExtension { 20 | var settingsScene: some AppExtensionScene { 21 | GenericScene(sceneID: "Settings") { 22 | Form { 23 | self.settings 24 | .environmentObject(self) 25 | } 26 | .formStyle(.grouped) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Sidebar/Inspector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 25/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | import ExtensionKit 10 | 11 | public struct Inspector: Sidebar { 12 | public var body: Never { 13 | fatalError() 14 | } 15 | 16 | var id: String 17 | 18 | public init(id: String, content: () -> Content) { 19 | self.id = id 20 | self.content = content() 21 | } 22 | 23 | @ViewBuilder var content: Content 24 | 25 | @_spi(CodeEdit) 26 | public func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { 27 | let scene = PrimitiveAppExtensionScene(id: id) { 28 | VStack(alignment: .leading) { 29 | content 30 | .scrollContentBackground(.hidden) 31 | .environmentObject(environment) 32 | } 33 | .frame(maxWidth: .infinity, maxHeight: .infinity) 34 | } 35 | let store = ResolvedSidebar.SidebarStore(sceneID: id, kind: .inspector) 36 | return [ResolvedSidebar(scene: scene, store: store)] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Sidebar/Navigator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 25/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | import ExtensionKit 10 | 11 | public struct Navigator: Sidebar { 12 | public var body: Never { 13 | fatalError() 14 | } 15 | 16 | var id: String 17 | 18 | public init(id: String, content: () -> Content) { 19 | self.id = id 20 | self.content = content() 21 | } 22 | 23 | @ViewBuilder var content: Content 24 | 25 | @_spi(CodeEdit) 26 | public func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { 27 | let scene = PrimitiveAppExtensionScene(id: id) { 28 | VStack(alignment: .leading) { 29 | content 30 | .scrollContentBackground(.hidden) 31 | .environmentObject(environment) 32 | } 33 | .frame(maxWidth: .infinity, maxHeight: .infinity) 34 | } 35 | let store = ResolvedSidebar.SidebarStore(sceneID: id, kind: .navigator) 36 | return [ResolvedSidebar(scene: scene, store: store)] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Sidebar/ResolvedSidebar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 24/03/2023. 6 | // 7 | 8 | import Foundation 9 | import ExtensionKit 10 | 11 | public struct ResolvedSidebar { 12 | 13 | public enum Kind: Codable, Hashable { 14 | case navigator, inspector 15 | } 16 | 17 | var scene: PrimitiveAppExtensionScene 18 | 19 | var store: SidebarStore 20 | 21 | public struct SidebarStore: Codable, Hashable { 22 | public var sceneID: String 23 | public var icon: String? 24 | public var help: String? 25 | public var kind: Kind 26 | } 27 | 28 | var extensionKind: ExtensionKind { 29 | .sidebarItem(data: store) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Sidebar/Sidebar+Help.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 24/03/2023. 6 | // 7 | 8 | import SwiftUI 9 | import ExtensionKit 10 | 11 | struct SidebarHelpModifier: Sidebar { 12 | var help: String 13 | var content: Content 14 | 15 | var body: Never { 16 | fatalError() 17 | } 18 | 19 | func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { 20 | var sidebars = self.content.resolve(environment: environment) 21 | for index in sidebars.indices { 22 | sidebars[index].store.help = help 23 | } 24 | return sidebars 25 | } 26 | } 27 | 28 | struct SidebarIconModifier: Sidebar { 29 | var icon: String 30 | var content: Content 31 | 32 | var body: Never { 33 | fatalError() 34 | } 35 | 36 | func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { 37 | var sidebars = self.content.resolve(environment: environment) 38 | for index in sidebars.indices { 39 | sidebars[index].store.icon = icon 40 | } 41 | return sidebars 42 | } 43 | } 44 | 45 | public extension Sidebar { 46 | func help(_ message: String) -> some Sidebar { 47 | SidebarHelpModifier(help: message, content: self) 48 | } 49 | 50 | func icon(_ systemName: String) -> some Sidebar { 51 | SidebarIconModifier(icon: systemName, content: self) 52 | } 53 | 54 | func title(_ title: String) -> some Sidebar { 55 | self 56 | } 57 | 58 | func description(_ message: String) -> some Sidebar { 59 | self 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Sidebar/Sidebar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 24/03/2023. 6 | // 7 | 8 | import Foundation 9 | import ExtensionKit 10 | 11 | public protocol Sidebar { 12 | associatedtype Body: Sidebar 13 | 14 | @SidebarBuilder 15 | var body: Body { get } 16 | 17 | @_spi(CodeEdit) 18 | func resolve(environment: some ObservableObject) -> [ResolvedSidebar] 19 | } 20 | 21 | public extension Sidebar { 22 | func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { 23 | body.resolve(environment: environment) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Sidebar/SidebarBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 24/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | @resultBuilder 11 | public struct SidebarBuilder { 12 | public static func buildPartialBlock(first: Never) -> Never {} 13 | 14 | public static func buildPartialBlock(first: some Sidebar) -> some Sidebar { 15 | first 16 | } 17 | 18 | public static func buildPartialBlock(accumulated: some Sidebar, next: some Sidebar) -> some Sidebar { 19 | TupleSidebar(accumulated, next) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Sidebar/SidebarExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 30/12/2022. 6 | // 7 | 8 | import Foundation 9 | import ExtensionKit 10 | import SwiftUI 11 | 12 | public protocol SidebarExtension: ObservableObject { 13 | 14 | associatedtype SidebarBody: Sidebar 15 | 16 | @SidebarBuilder 17 | var sidebars: SidebarBody { get } 18 | } 19 | 20 | public extension SidebarExtension { 21 | var sidebarScenes: some AppExtensionScene { 22 | sidebars.resolve(environment: self).map(\.scene) 23 | } 24 | 25 | var availableExtensions: [ExtensionKind] { 26 | sidebars.resolve(environment: self).map(\.extensionKind) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Sidebar/SidebarNever.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 24/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Swift.Never: Sidebar { 11 | 12 | public var body: Swift.Never { 13 | fatalError() 14 | } 15 | 16 | @_spi(CodeEdit) 17 | public func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { 18 | fatalError() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/UIExtensions/Sidebar/TupleSidebar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 24/03/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct TupleSidebar: Sidebar { 11 | var c0: C0 12 | var c1: C1 13 | 14 | init(_ c0: C0, _ c1: C1) { 15 | self.c0 = c0 16 | self.c1 = c1 17 | } 18 | 19 | var body: Never { 20 | fatalError() 21 | } 22 | 23 | func resolve(environment: some ObservableObject) -> [ResolvedSidebar] { 24 | c0.resolve(environment: environment) + c1.resolve(environment: environment) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/CodeEditKit/XPCWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Wouter Hennen on 30/12/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias XPCReply = (Data?, Error?) -> Void 11 | 12 | public enum XPCError: Error { 13 | case extensionDoesNotExist(description: String) 14 | case identifierDoesNotExist(description: String) 15 | } 16 | 17 | @objc 18 | public protocol XPCWrappable { 19 | func getExtensionURL(reply: @escaping XPCReply) 20 | 21 | func getExtensionKinds(reply: @escaping XPCReply) 22 | 23 | func getExtensionProcessIdentifier(reply: @escaping (Int32) -> Void) 24 | 25 | func doAction(with identifier: String, reply: @escaping XPCReply) 26 | 27 | func isDebug(reply: @escaping (Bool) -> Void) 28 | } 29 | 30 | class XPCWrapper: XPCWrappable { 31 | 32 | var ext: any CodeEditExtension 33 | 34 | init(_ ext: any CodeEditExtension) { 35 | self.ext = ext 36 | } 37 | 38 | func getExtensionURL(reply: @escaping XPCReply) { 39 | do { 40 | let encoded = try JSONEncoder().encode(ext.extensionURL) 41 | reply(encoded, nil) 42 | } catch { 43 | reply(nil, error) 44 | } 45 | } 46 | 47 | func getExtensionKinds(reply: @escaping XPCReply) { 48 | do { 49 | let encoded = try JSONEncoder().encode(ext.gatherAvailableExtensions()) 50 | reply(encoded, nil) 51 | } catch { 52 | reply(nil, error) 53 | } 54 | } 55 | 56 | func getExtensionProcessIdentifier(reply: @escaping (Int32) -> Void) { 57 | reply(ProcessInfo.processInfo.processIdentifier) 58 | } 59 | 60 | func doAction(with identifier: String, reply: @escaping XPCReply) { 61 | 62 | } 63 | 64 | func isDebug(reply: @escaping (Bool) -> Void) { 65 | #if DEBUG 66 | reply(true) 67 | #else 68 | reply(false) 69 | #endif 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/CodeEditKitTests/CEExtensionKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import CodeEditKit 3 | 4 | final class CEExtensionKitTests: XCTestCase { 5 | 6 | // This is only a sample test case 7 | // until real unit tests are established 8 | func testExample() throws { 9 | XCTAssertTrue(true) 10 | } 11 | } 12 | --------------------------------------------------------------------------------